Skip to main content

keelson_core/
query.rs

1use std::borrow::Cow;
2use std::fmt;
3
4use crate::dialect::Dialect;
5use crate::error::Result;
6use crate::expr::{IntoExpr, RawArg};
7use crate::value::{ToValue, Value};
8use crate::writer::{Expression, build, build_from};
9
10/// Which statement a query renders.
11///
12/// Carried so the execution layer can decide whether to expect rows without
13/// parsing the SQL back.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
15#[non_exhaustive]
16pub enum QueryType {
17    /// Not one of the four — a raw statement, a DDL statement, a bare expression.
18    #[default]
19    Unknown,
20    /// `SELECT`.
21    Select,
22    /// `INSERT`.
23    Insert,
24    /// `UPDATE`.
25    Update,
26    /// `DELETE`.
27    Delete,
28    /// `MERGE` — PostgreSQL's conditional insert/update/delete against a source.
29    ///
30    /// A fifth statement kind rather than a flavour of the four: whether it
31    /// returns rows depends on its `RETURNING` clause, exactly as for the three
32    /// mutations, but it is none of them.
33    Merge,
34}
35
36impl fmt::Display for QueryType {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        f.write_str(match self {
39            QueryType::Unknown => "UNKNOWN",
40            QueryType::Select => "SELECT",
41            QueryType::Insert => "INSERT",
42            QueryType::Update => "UPDATE",
43            QueryType::Delete => "DELETE",
44            QueryType::Merge => "MERGE",
45        })
46    }
47}
48
49/// A complete, runnable statement.
50///
51/// An [`Expression`] is any fragment; a `Query` is a fragment that stands alone,
52/// knows which dialect it is written in and therefore can be built without being
53/// told anything. That is what lets `q.build()` take no arguments while a bare
54/// expression still needs [`build(dialect, expr)`](build).
55///
56/// A query is also an expression, so it nests as a sub-select — and because the
57/// placeholder counter belongs to the [`SqlWriter`](crate::SqlWriter) rather than
58/// to the query, nesting re-indexes on its own.
59pub trait Query: Expression {
60    /// Which statement this renders.
61    fn query_type(&self) -> QueryType;
62
63    /// The dialect this query renders itself in.
64    ///
65    /// bob's `BaseQuery` carries its dialect the same way, and deliberately
66    /// ignores the dialect handed to it when embedded in another query.
67    fn dialect(&self) -> &dyn Dialect;
68
69    /// Render to SQL and arguments, numbering placeholders from 1.
70    ///
71    /// The escape hatch that is always open: whatever layer produced the query,
72    /// this hands back a `String` and a `Vec<Value>` and nothing else.
73    fn build(&self) -> Result<(String, Vec<Value>)> {
74        build(self.dialect(), self)
75    }
76
77    /// [`build`](Self::build) with a different first placeholder position, for
78    /// splicing into a statement that already has arguments — bob's `BuildN`.
79    fn build_from(&self, start: usize) -> Result<(String, Vec<Value>)> {
80        build_from(self.dialect(), start, self)
81    }
82}
83
84/// The execution layer's extension points, hung on the query rather than
85/// discovered by downcasting.
86///
87/// bob type-asserts a query against `HookableQuery`, `Loadable` and
88/// `MapperModder` at run time. Here the questions are trait methods, so they
89/// resolve statically: every one is defaulted to "none", a Layer 1 query opts in
90/// with an empty impl, and a generated Layer 2 query overrides only what it
91/// actually carries.
92///
93/// The three payloads are type parameters because their concrete shapes depend on
94/// the executor, which core knows nothing about — while the trait itself has to
95/// live here, since neither a dialect crate nor a backend crate could implement
96/// the other's trait for the other's type. Core fixes the mechanism; the
97/// execution layer fills in the types.
98pub trait QueryExtensions<Hook, Loader, MapperMod>: Query {
99    /// Hooks to run before this query executes.
100    fn hooks(&self) -> &[Hook] {
101        &[]
102    }
103
104    /// Loaders to run after this query executes, for eagerly loaded relations.
105    fn loaders(&self) -> &[Loader] {
106        &[]
107    }
108
109    /// Adjustments to the row mapper, for relations loaded in the same query.
110    fn mapper_mods(&self) -> &[MapperMod] {
111        &[]
112    }
113}
114
115/// A whole statement, written by hand.
116///
117/// The builder's counterpart to [`raw`](crate::expr::raw): that one is a
118/// *fragment* an expression accepts, this one is a *statement* nothing built
119/// it. It is an ordinary [`Query`], so the execution layer's verbs apply
120/// unchanged — `fetch_all::<T>()` maps hand-written SQL onto a struct exactly
121/// as it maps a built statement — and it is an [`Expression`], so it nests as
122/// a sub-select in a built statement like any other query.
123///
124/// Construct one from the dialect it is written in: `psql::raw_query(…)`,
125/// `mysql::raw_query(…)`, `sqlite::raw_query(…)` — or, when the SQL is a
126/// literal, through that dialect's `sql!` (feature `macros`), which writes the
127/// values at the holes they bind to and is the form to reach for first. This
128/// one is what `sql!` expands to, and what SQL arriving at run time — from a
129/// file, a migration runner — has to use, since a macro can only read a
130/// literal.
131///
132/// **Placeholders are `?`, on every dialect,** and are rewritten into that
133/// dialect's own syntax as the statement renders — `$1` on PostgreSQL, `?1` on
134/// SQLite, `?` on MySQL. Write `\?` for a literal question mark. The rule is
135/// [`template`](crate::expr::template)'s, for the same reason: a hand-written
136/// statement should not have to be respelled to move between engines, and a
137/// value still never reaches the SQL text. A `?` count that disagrees with the
138/// bound arguments is an error from [`build`](Query::build), not a silently
139/// misbound statement.
140///
141/// keelson does not parse what you hand it. Everything the builder guarantees
142/// — that the SQL is grammatical for the engine, that identifiers are quoted —
143/// is yours here; what you keep is the binding, the placeholder rewriting, the
144/// row mapping, the transaction, and the tracing span.
145///
146/// # Why this is untyped, and stays untyped
147///
148/// The row type is whatever you name in `fetch_all::<T>()`, and nothing checks
149/// it against the schema. That is the escape hatch's job description, not a
150/// gap waiting to be filled.
151///
152/// **The rejected alternative:** a proc macro that types the row and the
153/// parameters at the call site, by running keelson-gen's own inference against
154/// a committed schema snapshot at compile time. It would work — the analysis
155/// is already a library, and the snapshot would need no database at build
156/// time, which is more than sqlx's `query!` manages. It was rejected because
157/// it adds **no guarantee keelson does not already offer**: the same analysis,
158/// against the same schema, producing the same types, is Layer 4
159/// (`keelson-gen`'s `.sql` files). The only difference is where the SQL lives.
160/// Paying a build-time SQL parser (libpg_query, in C, for PostgreSQL), a new
161/// artifact to keep fresh, and a second answer to "typed hand-written SQL"
162/// buys locality and nothing else.
163///
164/// So the line is: **typed** SQL goes in a `.sql` file (Layer 4) or comes from
165/// a generated model (Layer 3), and both are typed because they were derived
166/// from the schema. **Untyped** SQL is this, and its counterpart for
167/// fragments. Wanting the compiler to reject a malformed query outright is a
168/// real want, and [diesel](https://diesel.rs) is the library that serves it.
169#[derive(Debug, Clone)]
170pub struct RawQuery<D> {
171    sql: Cow<'static, str>,
172    args: Vec<RawArg>,
173    dialect: D,
174    query_type: QueryType,
175}
176
177impl<D> RawQuery<D> {
178    /// The statement, in `dialect`'s SQL. Dialect crates wrap this as
179    /// `raw_query`, which is how a caller should reach it.
180    pub fn new(dialect: D, sql: impl Into<Cow<'static, str>>) -> Self {
181        RawQuery {
182            sql: sql.into(),
183            args: Vec::new(),
184            dialect,
185            query_type: QueryType::Unknown,
186        }
187    }
188
189    /// Bind a value to the next `?`.
190    #[must_use]
191    pub fn bind(mut self, value: impl ToValue) -> Self {
192        self.args.push(RawArg::value(value));
193        self
194    }
195
196    /// Bind every value in `values`, in order — one `?` each.
197    #[must_use]
198    pub fn bind_all<V: ToValue>(mut self, values: impl IntoIterator<Item = V>) -> Self {
199        self.args.extend(values.into_iter().map(RawArg::value));
200        self
201    }
202
203    /// Splice an expression into the next `?` instead of binding a value.
204    ///
205    /// This is what makes `WHERE id IN (?)` work: the expression consumes as
206    /// many placeholder positions as it binds arguments, and the counter keeps
207    /// going from there. A quoted identifier, a sub-query and a built
208    /// statement all go in this way.
209    #[must_use]
210    pub fn bind_expr(mut self, expression: impl IntoExpr) -> Self {
211        self.args.push(RawArg::expr(expression));
212        self
213    }
214
215    /// Declare which statement this is.
216    ///
217    /// It feeds the tracing span and nothing else — the execution layer never
218    /// polices it against the SQL. The default is
219    /// [`QueryType::Unknown`], which is the honest answer for text keelson has
220    /// not read: guessing from the leading keyword would be wrong for
221    /// `WITH … INSERT`, and keelson does not guess.
222    #[must_use]
223    pub fn kind(mut self, query_type: QueryType) -> Self {
224        self.query_type = query_type;
225        self
226    }
227}
228
229impl<D: fmt::Debug + Send + Sync> Expression for RawQuery<D> {
230    fn write_sql(&self, w: &mut crate::writer::SqlWriter<'_>) {
231        crate::expr::template(self.sql.clone(), self.args.iter().cloned()).write_sql(w);
232    }
233}
234
235impl<D: Dialect> Query for RawQuery<D> {
236    fn query_type(&self) -> QueryType {
237        self.query_type
238    }
239
240    fn dialect(&self) -> &dyn Dialect {
241        &self.dialect
242    }
243}
244
245// No hooks, no loaders, no mapper mods: a statement keelson did not build has
246// nothing hung on it. The empty impl is what makes the execution layer's verbs
247// available at all.
248impl<D: Dialect, H, L, M> QueryExtensions<H, L, M> for RawQuery<D> {}
249
250#[cfg(test)]
251mod tests {
252    use keelson_sqlcheck::testing::assert_stmt_sql;
253
254    use super::*;
255    use crate::dialect::testing::Numbered;
256    use crate::error::Error;
257    use crate::writer::SqlWriter;
258
259    #[test]
260    fn display_matches_the_sql_keyword() {
261        assert_eq!(QueryType::Select.to_string(), "SELECT");
262        assert_eq!(QueryType::Insert.to_string(), "INSERT");
263        assert_eq!(QueryType::Update.to_string(), "UPDATE");
264        assert_eq!(QueryType::Delete.to_string(), "DELETE");
265        assert_eq!(QueryType::Merge.to_string(), "MERGE");
266        assert_eq!(QueryType::Unknown.to_string(), "UNKNOWN");
267        assert_eq!(QueryType::default(), QueryType::Unknown);
268    }
269
270    /// The shape a dialect crate's query struct will have. It renders a whole
271    /// statement, so the cases below go to the judge — and it names a table from
272    /// `tests/schema/psql.sql` so that a real server can resolve it.
273    #[derive(Debug)]
274    struct Select {
275        table: &'static str,
276        min_age: i32,
277    }
278
279    impl Expression for Select {
280        fn write_sql(&self, w: &mut SqlWriter<'_>) {
281            w.push_str("SELECT * FROM ");
282            w.push_quoted(&[self.table]);
283            w.push_str(" WHERE ");
284            w.push_quoted(&["age"]);
285            w.push_str(" >= ");
286            w.push_arg(self.min_age);
287        }
288    }
289
290    impl Query for Select {
291        fn query_type(&self) -> QueryType {
292            QueryType::Select
293        }
294
295        fn dialect(&self) -> &dyn Dialect {
296            &Numbered
297        }
298    }
299
300    #[test]
301    fn a_query_builds_itself_without_being_told_the_dialect() {
302        let q = Select {
303            table: "users",
304            min_age: 21,
305        };
306        let (sql, args) = q.build().unwrap();
307        assert_stmt_sql(&sql, r#"SELECT * FROM "users" WHERE "age" >= $1"#);
308        assert_eq!(args, vec![Value::I32(21)]);
309        assert_eq!(q.query_type(), QueryType::Select);
310
311        // Not judged: a statement whose lowest placeholder is `$4` has no `$1`, and
312        // a server refuses to prepare that. Which is the point of `build_from` —
313        // the result is a fragment for splicing into a statement that already has
314        // three arguments, not something to send on its own.
315        let (sql, _) = q.build_from(4).unwrap();
316        assert_eq!(sql, r#"SELECT * FROM "users" WHERE "age" >= $4"#);
317    }
318
319    #[test]
320    fn a_query_is_usable_erased() {
321        let q: Box<dyn Query> = Box::new(Select {
322            table: "users",
323            min_age: 1,
324        });
325        assert_eq!(q.query_type(), QueryType::Select);
326        assert!(q.build().is_ok());
327    }
328
329    #[test]
330    fn a_query_nested_in_another_shares_the_numbering() {
331        #[derive(Debug)]
332        struct Wrapper(Select);
333
334        impl Expression for Wrapper {
335            fn write_sql(&self, w: &mut SqlWriter<'_>) {
336                w.push_str("SELECT * FROM (");
337                w.write_expr(&self.0);
338                // The alias is not decoration: PostgreSQL requires one on a
339                // sub-query in a FROM.
340                w.push_str(") AS \"u\" WHERE \"u\".\"id\" = ");
341                w.push_arg(9i32);
342            }
343        }
344
345        let (sql, args) = build(
346            &Numbered,
347            &Wrapper(Select {
348                table: "users",
349                min_age: 21,
350            }),
351        )
352        .unwrap();
353        assert_stmt_sql(
354            &sql,
355            concat!(
356                r#"SELECT * FROM (SELECT * FROM "users" WHERE "age" >= $1) AS "u" "#,
357                r#"WHERE "u"."id" = $2"#
358            ),
359        );
360        assert_eq!(args, vec![Value::I32(21), Value::I32(9)]);
361    }
362
363    // A Layer 1 query opts in with an empty impl and answers "no extensions" for
364    // whatever payload types the execution layer picks.
365    impl<H, L, M> QueryExtensions<H, L, M> for Select {}
366
367    #[test]
368    fn extension_points_default_to_none_without_any_downcasting() {
369        let q = Select {
370            table: "users",
371            min_age: 1,
372        };
373        let q: &dyn QueryExtensions<&'static str, u8, u8> = &q;
374        assert!(q.hooks().is_empty());
375        assert!(q.loaders().is_empty());
376        assert!(q.mapper_mods().is_empty());
377    }
378
379    // ── RawQuery: a whole statement nobody built ──────────────────────────
380    //
381    // Judged like any other statement: what a caller hands in is what the
382    // engine has to accept, and `?` rewriting is the only thing keelson does
383    // to it.
384
385    #[test]
386    fn a_hand_written_statement_binds_and_rewrites_its_placeholders() {
387        let q = RawQuery::new(Numbered, "SELECT * FROM \"users\" WHERE \"age\" >= ?")
388            .bind(21)
389            .kind(QueryType::Select);
390        let (sql, args) = q.build().unwrap();
391        assert_stmt_sql(&sql, r#"SELECT * FROM "users" WHERE "age" >= $1"#);
392        assert_eq!(args, vec![Value::I32(21)]);
393        assert_eq!(q.query_type(), QueryType::Select);
394    }
395
396    #[test]
397    fn the_statement_kind_is_unknown_until_it_is_declared() {
398        // keelson has not read the text, so it does not claim to know. Guessing
399        // from the leading keyword would be wrong for `WITH … INSERT`.
400        let q = RawQuery::new(Numbered, "SELECT 1");
401        assert_eq!(q.query_type(), QueryType::Unknown);
402    }
403
404    #[test]
405    fn binding_more_than_the_placeholders_is_an_error_not_a_misbound_statement() {
406        let q = RawQuery::new(Numbered, "SELECT * FROM \"users\" WHERE \"age\" >= ?")
407            .bind(21)
408            .bind(22);
409        let err = q.build().unwrap_err();
410        assert!(matches!(err, Error::RawArgCount { .. }), "{err}");
411    }
412
413    #[test]
414    fn an_expression_can_be_spliced_where_a_value_would_go() {
415        // The `IN (?)` case: the spliced expression consumes as many
416        // placeholder positions as it binds, and the counter carries on.
417        let q = RawQuery::new(
418            Numbered,
419            "SELECT * FROM \"users\" WHERE \"id\" IN (?) AND \"age\" >= ?",
420        )
421        .bind_expr(crate::expr::args([1, 2, 3]))
422        .bind(21);
423        let (sql, args) = q.build().unwrap();
424        assert_stmt_sql(
425            &sql,
426            r#"SELECT * FROM "users" WHERE "id" IN ($1, $2, $3) AND "age" >= $4"#,
427        );
428        assert_eq!(
429            args,
430            vec![Value::I32(1), Value::I32(2), Value::I32(3), Value::I32(21)]
431        );
432    }
433
434    #[test]
435    fn a_hand_written_statement_nests_in_a_built_one() {
436        // It is an `Expression`, so it goes anywhere a query goes — and the
437        // outer writer renumbers it, exactly as for a built sub-select.
438        let (sql, args) = build(
439            &Numbered,
440            &Select {
441                table: "users",
442                min_age: 21,
443            }
444            .wrapped_around(
445                RawQuery::new(Numbered, "SELECT \"id\" FROM \"posts\" WHERE \"views\" > ?")
446                    .bind(100),
447            ),
448        )
449        .unwrap();
450        assert_stmt_sql(
451            &sql,
452            concat!(
453                r#"SELECT * FROM "users" WHERE "age" >= $1 AND "id" IN "#,
454                r#"(SELECT "id" FROM "posts" WHERE "views" > $2)"#
455            ),
456        );
457        assert_eq!(args, vec![Value::I32(21), Value::I32(100)]);
458    }
459
460    /// `Select`, with a sub-query hung on its `WHERE` — just enough to prove
461    /// the nesting, without a second statement type.
462    #[derive(Debug)]
463    struct Wrapped(Select, RawQuery<Numbered>);
464
465    impl Select {
466        fn wrapped_around(self, inner: RawQuery<Numbered>) -> Wrapped {
467            Wrapped(self, inner)
468        }
469    }
470
471    impl Expression for Wrapped {
472        fn write_sql(&self, w: &mut SqlWriter<'_>) {
473            self.0.write_sql(w);
474            w.push_str(" AND ");
475            w.push_quoted(&["id"]);
476            w.push_str(" IN (");
477            self.1.write_sql(w);
478            w.push_str(")");
479        }
480    }
481}