toasty_core/driver.rs
1//! Database driver interface for Toasty.
2//!
3//! This module defines the traits and types that database drivers must implement
4//! to integrate with the Toasty query engine. The two core traits are [`Driver`]
5//! (factory for connections and schema operations) and [`Connection`] (executes
6//! operations against a live database session).
7//!
8//! The query planner inspects [`Capability`] to decide which [`Operation`]
9//! variants to emit. SQL-based drivers receive [`Operation::QuerySql`] and
10//! [`Operation::Insert`], while key-value drivers (e.g., DynamoDB) receive
11//! [`Operation::GetByKey`], [`Operation::QueryPk`], etc. The
12//! [`SchemaMutations`] sub-struct (`Capability::schema_mutations`) describes
13//! what the database can do to its own schema — for example, whether
14//! `ALTER COLUMN` can change a column's type — and the migration generator
15//! consults it to decide between an in-place alter and a table rebuild.
16//!
17//! # Architecture
18//!
19//! ```text
20//! Query Engine ──▶ Operation ──▶ Connection::exec() ──▶ ExecResponse
21//! ▲
22//! │
23//! Driver::capability()
24//! ```
25//!
26//! # Error classification
27//!
28//! The pool and the engine branch on the error variant returned from
29//! [`Connection::exec`] and [`Connection::ping`]. Drivers MUST cooperate
30//! with those branches:
31//!
32//! - A connection-level fault (closed socket, broken pipe, protocol
33//! error, end-of-stream during handshake) MUST be classified as
34//! [`crate::Error::connection_lost`]. The pool uses that signal to
35//! evict the slot and to wake the background sweep, which then pings
36//! the remaining idle connections and drops any that also fail. Any
37//! other error variant for the same condition leaks a dead connection
38//! back into the pool.
39//!
40//! - A retryable transaction conflict (PostgreSQL SQLSTATE `40001`,
41//! MySQL error `1213`) SHOULD be classified as
42//! [`crate::Error::serialization_failure`]. The engine does not retry
43//! automatically; the classification is propagated to user code so
44//! the caller can decide.
45//!
46//! - A write attempted against a read-only session (PostgreSQL
47//! `25006`, MySQL `1792`) SHOULD be classified as
48//! [`crate::Error::read_only_transaction`].
49//!
50//! Other backend errors are typically wrapped with
51//! [`crate::Error::driver_operation_failed`].
52
53mod capability;
54pub use capability::{Capability, SchemaMutations, StorageTypes};
55
56mod response;
57pub use response::{ExecResponse, Rows};
58
59pub mod operation;
60pub use operation::{IsolationLevel, Operation};
61
62use crate::schema::{
63 Schema,
64 db::{AppliedMigration, Migration},
65 diff,
66};
67
68use async_trait::async_trait;
69
70use std::{borrow::Cow, fmt::Debug, sync::Arc};
71
72/// Factory for database connections and provider of driver-level metadata.
73///
74/// Each database backend (SQLite, PostgreSQL, MySQL, DynamoDB) implements this
75/// trait to tell Toasty what the backend supports ([`Capability`]) and to
76/// create [`Connection`] instances on demand.
77///
78/// # Examples
79///
80/// ```ignore
81/// use toasty_core::driver::Driver;
82///
83/// // Drivers are typically constructed from a connection URL:
84/// let driver: Box<dyn Driver> = make_driver("sqlite::memory:").await;
85/// assert!(!driver.url().is_empty());
86///
87/// let capability = driver.capability();
88/// assert!(capability.sql);
89///
90/// let conn = driver.connect().await.unwrap();
91/// ```
92#[async_trait]
93pub trait Driver: Debug + Send + Sync + 'static {
94 /// Returns the URL this driver is connecting to.
95 fn url(&self) -> Cow<'_, str>;
96
97 /// Describes the driver's capability, which informs the query planner.
98 fn capability(&self) -> &'static Capability;
99
100 /// Creates a new connection to the database.
101 ///
102 /// This method is called by the [`Pool`] whenever a [`Connection`] is requested while none is
103 /// available and there is room to create a new [`Connection`].
104 async fn connect(&self) -> crate::Result<Box<dyn Connection>>;
105
106 /// Returns the maximum number of simultaneous database connections supported. For example,
107 /// this is `Some(1)` for the in-memory SQLite driver which cannot be pooled.
108 fn max_connections(&self) -> Option<usize> {
109 None
110 }
111
112 /// Generates a migration from a [`diff::Schema`].
113 fn generate_migration(&self, schema_diff: &diff::Schema<'_>) -> Migration;
114
115 /// Drops the entire database and recreates an empty one without applying migrations.
116 ///
117 /// Used primarily in tests to start with a clean slate.
118 async fn reset_db(&self) -> crate::Result<()>;
119}
120
121/// A live database session that can execute [`Operation`]s.
122///
123/// Connections are obtained from [`Driver::connect`] and are managed by the
124/// connection pool. All query execution flows through [`Connection::exec`],
125/// which accepts an [`Operation`] and returns an [`ExecResponse`].
126///
127/// # Examples
128///
129/// ```ignore
130/// use toasty_core::driver::{Connection, Operation, ExecResponse};
131/// use toasty_core::driver::operation::Transaction;
132///
133/// // Execute a transaction start operation on a connection:
134/// let response = conn.exec(&schema, Transaction::start().into()).await?;
135/// ```
136#[async_trait]
137pub trait Connection: Debug + Send + 'static {
138 /// Executes a database operation and returns the result.
139 ///
140 /// This is the single entry point for all database interactions. The
141 /// query engine compiles user queries into [`Operation`] values and
142 /// dispatches them here. The driver translates each operation into
143 /// backend-specific calls and returns an [`ExecResponse`].
144 async fn exec(&mut self, schema: &Arc<Schema>, plan: Operation) -> crate::Result<ExecResponse>;
145
146 /// Cheap, synchronous, local check that the driver's client object
147 /// still considers the connection open.
148 ///
149 /// Examples: a flag the driver flips when its background reader
150 /// reports a socket close (the MySQL driver does this), an
151 /// `is_closed()` accessor on the underlying client. Implementations
152 /// must not block and must not perform I/O — the check runs on the
153 /// hot path of every recycle and must complete in nanoseconds.
154 /// Drivers that cannot answer cheaply leave this at the default and
155 /// rely on the pool's [`ping`](Self::ping) sweep or the per-acquire
156 /// pre-ping option to catch a dead connection.
157 ///
158 /// The pool consults `is_valid()` whenever a connection is returned
159 /// to the idle set. A `false` result causes the slot to be dropped
160 /// before another caller can pick it up; the pool then returns
161 /// another idle connection or opens a fresh one. A connection is
162 /// also re-checked immediately after every [`Connection::exec`]; if
163 /// the operation flipped the flag (e.g. the driver classified the
164 /// error as connection-lost and updated its state), the worker task
165 /// exits and the slot is evicted.
166 ///
167 /// The default returns `true`. Drivers without a usable passive
168 /// signal stay on this default and rely on the active path: an
169 /// operation surfaces [`crate::Error::connection_lost`], the pool
170 /// drops the slot, and the background sweep eagerly pings the rest
171 /// of the idle pool.
172 fn is_valid(&self) -> bool {
173 true
174 }
175
176 /// Active liveness probe. The pool's background health-check sweep
177 /// calls this on the longest-idle connection on every tick, and on
178 /// every other idle connection when an escalation is triggered.
179 /// When `pool_pre_ping` is enabled, the pool also calls it on every
180 /// acquire.
181 ///
182 /// Drivers MUST classify a failure here as
183 /// [`crate::Error::connection_lost`] rather than a generic operation
184 /// error. The pool branches on that classification to drop the slot
185 /// (vs. returning it to the idle set after a transient query
186 /// error), and a user-observed `connection_lost` is what wakes the
187 /// pool's sweep to eagerly check the rest of the pool. Returning
188 /// any other error variant from `ping` will leak a dead connection
189 /// back into rotation.
190 ///
191 /// Drivers SHOULD make this the cheapest round-trip the backend
192 /// supports (`SELECT 1`, `COM_PING`, etc.). A ping that runs slower
193 /// than the sweep's per-call timeout (5 seconds, internal) is
194 /// treated as failed.
195 ///
196 /// The default returns `Ok(())` without doing any I/O. That is the
197 /// right answer for drivers whose connection layer cannot fail in
198 /// isolation (the in-process SQLite driver) or whose backend
199 /// manages its own pool beneath this surface (DynamoDB, where each
200 /// `exec` is an HTTP call with its own retry policy).
201 async fn ping(&mut self) -> crate::Result<()> {
202 Ok(())
203 }
204
205 /// Creates tables and indices defined in the schema on the database.
206 /// TODO: This will probably use database introspection in the future.
207 async fn push_schema(&mut self, _schema: &Schema) -> crate::Result<()>;
208
209 /// Returns a list of currently applied database migrations.
210 async fn applied_migrations(&mut self) -> crate::Result<Vec<AppliedMigration>>;
211
212 /// Applies a single migration to the database and records it as applied.
213 async fn apply_migration(
214 &mut self,
215 id: u64,
216 name: &str,
217 migration: &Migration,
218 ) -> crate::Result<()>;
219}