Skip to main content

keelson_exec/
executor.rs

1use std::fmt;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5
6use keelson_core::{Query, QueryType, Value};
7
8use crate::error::ExecError;
9use crate::row::Row;
10
11/// The boxed future every trait method returns.
12///
13/// Plain `std`, no futures-crate dependency. `'a` is the transient borrow of
14/// the executor for the duration of one call — the same class of lifetime as
15/// `SqlWriter<'_>`, and the only one this crate's public surface has.
16pub type ExecFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
17
18/// Which engine family an executor talks to.
19///
20/// Metadata, not dispatch: it feeds observability (`db.system`) and the
21/// round-trip harness. Backends are crates; nothing branches on this at run
22/// time to change behaviour.
23#[non_exhaustive]
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum Family {
26    /// PostgreSQL.
27    Postgres,
28    /// MySQL.
29    MySql,
30    /// SQLite.
31    Sqlite,
32}
33
34impl Family {
35    /// The OTel `db.system` value for this family.
36    pub fn as_str(self) -> &'static str {
37        match self {
38            Family::Postgres => "postgresql",
39            Family::MySql => "mysql",
40            Family::Sqlite => "sqlite",
41        }
42    }
43}
44
45impl fmt::Display for Family {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.write_str(self.as_str())
48    }
49}
50
51/// What crosses the executor boundary: exactly what `build()` produces, plus
52/// the statement-kind hint core already carries.
53///
54/// Owned, so an executor's future borrows nothing from the caller's query.
55/// Constructing one by hand — [`Statement::new`] — is the raw-SQL escape
56/// hatch, mirroring core's "the `build()` seam is always open": the SQL is
57/// passed to the driver verbatim, in the backend's own placeholder syntax,
58/// with the arguments bound per `docs/type-mappings.md`.
59#[derive(Debug, Clone, PartialEq)]
60#[non_exhaustive]
61pub struct Statement {
62    /// The SQL text, placeholders included.
63    pub sql: String,
64    /// The arguments, one per placeholder.
65    pub args: Vec<Value>,
66    /// Which statement this is. Feeds tracing; never policed against the SQL.
67    pub query_type: QueryType,
68}
69
70impl Statement {
71    /// A raw statement. `sql` is sent to the driver verbatim.
72    pub fn new(sql: impl Into<String>, args: Vec<Value>) -> Self {
73        Statement {
74            sql: sql.into(),
75            args,
76            query_type: QueryType::Unknown,
77        }
78    }
79
80    /// Build a query into a statement. This is the only path the
81    /// [`Execute`](crate::Execute) verbs use.
82    pub fn from_query(q: &(impl Query + ?Sized)) -> Result<Self, ExecError> {
83        let (sql, args) = q.build()?;
84        Ok(Statement {
85            sql,
86            args,
87            query_type: q.query_type(),
88        })
89    }
90}
91
92/// What a side-effect statement reports back.
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct ExecResult {
96    /// How many rows the statement changed.
97    pub rows_affected: u64,
98    /// The auto-increment id of the inserted row — MySQL and SQLite only.
99    /// PostgreSQL answers `None`: use `RETURNING` there, which is the honest
100    /// cross-engine story rather than a pretend-portable one.
101    pub last_insert_id: Option<i64>,
102}
103
104impl ExecResult {
105    /// Assemble a result. Backend-facing (the struct is `#[non_exhaustive]`).
106    pub fn new(rows_affected: u64, last_insert_id: Option<i64>) -> Self {
107        ExecResult {
108            rows_affected,
109            last_insert_id,
110        }
111    }
112}
113
114/// Anything that can run a built statement: a pool, a connection, a
115/// [`Transaction`](crate::Transaction).
116///
117/// Object-safe on purpose — `&dyn Executor` is the currency application code,
118/// generated models and hooks trade in — and `&self` on purpose: exclusivity
119/// is the *executor's* problem (a pool checks out per call; a transaction
120/// serialises behind a lock), not every call site's. The promise is
121/// deliberately weak: each call runs on *some* connection. Only a connection
122/// or a transaction strengthens that to *the same* connection, which is why
123/// session state (`SET`, temp tables, advisory locks) through a bare pool is
124/// a bug.
125///
126/// Backends implement these three methods and nothing else here; every
127/// ergonomic path ([`Execute`](crate::Execute)) funnels into them. New
128/// capabilities arrive as new opt-in traits ([`StreamExecutor`] is the
129/// template), never as added methods — adding a method here breaks every
130/// backend.
131pub trait Executor: Send + Sync + fmt::Debug {
132    /// Which engine family this executor talks to.
133    fn family(&self) -> Family;
134
135    /// Run a statement and collect every row — `SELECT`, or any mutation with
136    /// `RETURNING`.
137    fn fetch(&self, stmt: Statement) -> ExecFuture<'_, Result<Vec<Row>, ExecError>>;
138
139    /// Run a statement for its side effect.
140    fn execute(&self, stmt: Statement) -> ExecFuture<'_, Result<ExecResult, ExecError>>;
141}
142
143impl<E: Executor + ?Sized> Executor for &E {
144    fn family(&self) -> Family {
145        (**self).family()
146    }
147
148    fn fetch(&self, stmt: Statement) -> ExecFuture<'_, Result<Vec<Row>, ExecError>> {
149        (**self).fetch(stmt)
150    }
151
152    fn execute(&self, stmt: Statement) -> ExecFuture<'_, Result<ExecResult, ExecError>> {
153        (**self).execute(stmt)
154    }
155}
156
157impl<E: Executor + ?Sized> Executor for Arc<E> {
158    fn family(&self) -> Family {
159        (**self).family()
160    }
161
162    fn fetch(&self, stmt: Statement) -> ExecFuture<'_, Result<Vec<Row>, ExecError>> {
163        (**self).fetch(stmt)
164    }
165
166    fn execute(&self, stmt: Statement) -> ExecFuture<'_, Result<ExecResult, ExecError>> {
167        (**self).execute(stmt)
168    }
169}
170
171impl<E: Executor + ?Sized> Executor for Box<E> {
172    fn family(&self) -> Family {
173        (**self).family()
174    }
175
176    fn fetch(&self, stmt: Statement) -> ExecFuture<'_, Result<Vec<Row>, ExecError>> {
177        (**self).fetch(stmt)
178    }
179
180    fn execute(&self, stmt: Statement) -> ExecFuture<'_, Result<ExecResult, ExecError>> {
181        (**self).execute(stmt)
182    }
183}
184
185/// Opt-in streaming. A backend that can stream implements it; nothing
186/// requires it, because drivers differ too much here for it to belong in the
187/// minimum contract (native streams borrow their connection; ours must not).
188pub trait StreamExecutor: Executor {
189    /// Run a statement and hand rows back incrementally.
190    ///
191    /// The returned [`RowStream`] is owned — dropping it cancels the producer
192    /// and releases whatever connection it was riding.
193    fn fetch_stream(&self, stmt: Statement) -> ExecFuture<'_, Result<RowStream, ExecError>>;
194}
195
196/// An owned stream of rows (house rule: no lifetime parameter).
197///
198/// A bounded channel fed by a producer the backend runs; dropping the stream
199/// closes the channel, which the producer observes as its signal to stop and
200/// release its connection. Deliberately a concrete struct rather than
201/// `impl Stream`, so the futures crate stays out of the public API; a `Stream`
202/// impl can be added behind a feature later without breaking anything.
203pub struct RowStream {
204    rx: tokio::sync::mpsc::Receiver<Result<Row, ExecError>>,
205}
206
207impl fmt::Debug for RowStream {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        f.debug_struct("RowStream").finish_non_exhaustive()
210    }
211}
212
213impl RowStream {
214    /// Wrap a channel a backend feeds. Backend-facing.
215    pub fn new(rx: tokio::sync::mpsc::Receiver<Result<Row, ExecError>>) -> Self {
216        RowStream { rx }
217    }
218
219    /// The next row, or `None` when the result set is exhausted.
220    pub async fn next(&mut self) -> Option<Result<Row, ExecError>> {
221        self.rx.recv().await
222    }
223}