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