Skip to main content

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