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}