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}