keelson_core/clause/mod.rs
1//! The SQL clauses the three dialects share, as data.
2//!
3//! One struct per clause — [`With`], [`Where`], [`Frame`], … — each an
4//! [`Expression`] that renders **only itself**. A dialect's
5//! query type composes them as named fields and decides the order and the
6//! separation between them; nothing here knows what statement it is part of.
7//!
8//! # Four conventions the dialect crates depend on
9//!
10//! 1. **[`Default`] means "clause absent", and an absent clause renders nothing
11//! at all** — not a bare keyword, not a stray space. That is what lets a query
12//! write `w.write_if(!q.where_.is_empty(), " ", &q.where_, "")` and never think
13//! about it again. Every clause also has an `is_empty`, for the cases where the
14//! query needs a separator in front.
15//! 2. **A clause writes its own keyword**, so `Where` renders `WHERE a AND b` and
16//! `Limit` renders `LIMIT 10`. The single exception is [`Set`], and the reason
17//! is grammatical: MySQL's `ON DUPLICATE KEY UPDATE` takes the same assignment
18//! list with no `SET` in front of it, so the keyword has to belong to whatever
19//! contains the list. Clauses that are lists of independent items ([`Locks`],
20//! [`Combines`]) write no keyword either, because each item carries its own.
21//! 3. **Anything the caller supplies is an [`Expr`](crate::expr::Expr)**, never a
22//! `String` or a number: a `LIMIT` may be a bound argument, a `VALUES` cell may
23//! be a sub-select, a frame bound may be `$1 PRECEDING`. Fields typed
24//! `Cow<'static, str>` are *identifiers* and are quoted with the dialect's own
25//! quoting; fields typed as a keyword enum are fixed SQL.
26//! 4. **A nested context implements the same `Has*` trait.** `ON CONFLICT … DO
27//! UPDATE` has a `WHERE` of its own, and so does the index-inference target in
28//! front of it, so [`HasWhere`] is implemented by [`ConflictClause`] and
29//! [`ConflictTarget`] as well as by whatever statements a dialect gives one to.
30//! A mod written once therefore works in all of them.
31//!
32//! # Keywords are enums, not strings
33//!
34//! bob stores `Type string` / `Direction string` and exports `const` vocabularies.
35//! Here a closed keyword set is an enum ([`FrameMode`], [`LockStrength`],
36//! [`SetOp`], …), because the set really is closed by the grammar and a typo in
37//! one is otherwise a runtime syntax error. The two genuinely open-ended ones keep
38//! an escape hatch shaped like the grammar that opened them:
39//! [`JoinKind::Custom`] for MySQL's `STRAIGHT_JOIN`, and
40//! [`OrderDirection::Using`] for PostgreSQL's `ORDER BY … USING <operator>`.
41//!
42//! # Sub-queries
43//!
44//! Where a clause holds one — [`Cte::query`], [`Combine::query`],
45//! [`Values::query`] — it holds it as an [`Expr`](crate::expr::Expr), which a
46//! dialect's query type reaches through
47//! [`Expr::Custom`](crate::expr::Expr::Custom). bob has a `Query` interface in
48//! those slots whose only extra promise is "renders with its own dialect rather
49//! than the one handed to it", and a query type keeps that promise for itself by
50//! calling
51//! [`SqlWriter::write_with_dialect`](crate::SqlWriter::write_with_dialect) inside
52//! its own `write_sql`.
53//!
54//! # Rendering choices made here
55//!
56//! Formatting is not part of the contract — the golden comparison collapses runs
57//! of whitespace — but two choices are visible through it and are deliberate:
58//!
59//! - **Identifiers are quoted.** A CTE name, a `USING` column, a window name, a
60//! locked table: bob writes all of these verbatim, which breaks the moment one
61//! of them is a reserved word or mixed-case. They go through
62//! [`SqlWriter::push_quoted`](crate::SqlWriter::push_quoted) here.
63//! - **No trailing or doubled separators.** bob emits `FOR KEY SHARE ` and
64//! `USING("id") ` with trailing spaces and `PARTITION BY a ORDER BY b` with
65//! two, because each fragment guesses at its own padding. Every clause here
66//! writes separators only between things that are actually present.
67
68mod combine;
69mod conflict;
70mod cte;
71mod fetch;
72mod frame;
73mod from;
74mod group_by;
75mod having;
76mod join;
77mod limit;
78mod lock;
79mod offset;
80mod order_by;
81mod returning;
82mod select;
83mod set;
84mod values;
85mod where_;
86mod window;
87mod with;
88
89pub use combine::{Combine, Combines, HasCombines, SetOp};
90pub use conflict::{
91 Conflict, ConflictAction, ConflictClause, ConflictTarget, HasConflict, HasConflictClause,
92};
93pub use cte::{Cte, CteCycle, CteSearch, SearchOrder};
94pub use fetch::{Fetch, FirstOrNext, HasFetch};
95pub use frame::{Frame, FrameExclusion, FrameMode, HasFrame};
96pub use from::{
97 HasTableRef, IndexHint, IndexHintKind, IndexHintScope, IndexedBy, TableFunctions, TableRef,
98};
99pub use group_by::{GroupBy, GroupByWith, GroupingSet, GroupingSetKind, HasGroupBy};
100pub use having::{HasHaving, Having};
101pub use join::{HasJoins, Join, JoinKind};
102pub use limit::{HasLimit, Limit};
103pub use lock::{HasLocks, Lock, LockStrength, LockWait, Locks};
104pub use offset::{HasOffset, Offset, RowsKeyword};
105pub use order_by::{HasOrderBy, NullsPosition, OrderBy, OrderDef, OrderDirection};
106pub use returning::{HasReturning, Returning};
107pub use select::{HasSelectList, SelectList};
108pub use set::{HasSet, Set};
109pub use values::{HasValues, Values, ValuesRow};
110pub use where_::{HasWhere, Where};
111pub use window::{HasWindow, HasWindows, NamedWindow, Window, Windows};
112pub use with::{HasWith, With};
113
114use std::borrow::Cow;
115
116use crate::writer::{Expression, SqlWriter};
117
118/// An item of a clause list that may itself be absent.
119///
120/// The lists in this module hold structs rather than expressions, and an absent
121/// struct renders nothing — so [`SqlWriter::write_slice`] would put a separator
122/// between two things one of which is not there. For a list of independent items
123/// that is a stray space; for `WITH a AS (…), <absent>` it is a syntax error.
124trait MaybeAbsent {
125 /// Whether this item renders nothing.
126 fn is_absent(&self) -> bool;
127}
128
129/// [`SqlWriter::write_slice`] over items that may be absent: separators go only
130/// between items that are actually written, and if every item is absent then
131/// nothing at all is — affixes included.
132fn write_present<E: Expression + MaybeAbsent>(
133 w: &mut SqlWriter<'_>,
134 items: &[E],
135 prefix: &str,
136 sep: &str,
137 suffix: &str,
138) {
139 let mut written = false;
140 for item in items.iter().filter(|i| !i.is_absent()) {
141 w.push_str(if written { sep } else { prefix });
142 w.write_expr(item);
143 written = true;
144 }
145 if written {
146 w.push_str(suffix);
147 }
148}
149
150/// Write a list of identifiers, each quoted, wrapped in `prefix`/`suffix`.
151///
152/// Nothing at all is written when the list is empty — the same omission rule as
153/// [`SqlWriter::write_slice`], which cannot be used here because a
154/// `Cow<'static, str>` renders as raw SQL rather than as a quoted name.
155fn write_quoted_list(
156 w: &mut SqlWriter<'_>,
157 names: &[Cow<'static, str>],
158 prefix: &str,
159 sep: &str,
160 suffix: &str,
161) {
162 if names.is_empty() {
163 return;
164 }
165 w.push_str(prefix);
166 for (i, name) in names.iter().enumerate() {
167 if i > 0 {
168 w.push_str(sep);
169 }
170 w.push_quoted(&[name]);
171 }
172 w.push_str(suffix);
173}
174
175#[cfg(test)]
176mod tests {
177 use keelson_sqlcheck::testing::{assert_frag_sql, assert_stmt_sql};
178
179 use super::*;
180 use crate::dialect::testing::Numbered;
181 use crate::expr::{Chain, Expr, arg, quote};
182 use crate::value::Value;
183 use crate::writer::{Expression, SqlWriter, build};
184
185 /// Enough of a PostgreSQL `SELECT` to prove the clauses compose, with the
186 /// write order taken from
187 /// <https://www.postgresql.org/docs/17/sql-select.html>: `WITH`, select list,
188 /// `FROM`, `WHERE`, `GROUP BY`, `HAVING`, `WINDOW`, then the set operations
189 /// and their trailing `ORDER BY`/`LIMIT`.
190 ///
191 /// The real query type lives in `keelson-psql`; this one exists so the clause
192 /// shapes can be checked as a group from inside this crate. Because it renders
193 /// a whole statement, the cases below go to the judge rather than to
194 /// `assert_eq!` alone.
195 #[derive(Debug, Default)]
196 struct Select {
197 with: With,
198 select: SelectList,
199 from: TableRef,
200 where_: Where,
201 group_by: GroupBy,
202 having: Having,
203 windows: Windows,
204 order_by: OrderBy,
205 limit: Limit,
206 locks: Locks,
207 combines: Combines,
208 }
209
210 impl Expression for Select {
211 fn write_sql(&self, w: &mut SqlWriter<'_>) {
212 w.write_if(!self.with.is_empty(), "", &self.with, " ");
213
214 // The leading query is parenthesised exactly when it carries a tail
215 // clause of its own *and* something is combined onto it, so that
216 // `(SELECT … LIMIT 1) UNION (…)` cannot be read as the LIMIT applying
217 // to the union. See `Combines::parenthesises_leading_query`.
218 let tail =
219 !self.order_by.is_empty() || !self.limit.is_empty() || !self.locks.is_empty();
220 let parens = self.combines.parenthesises_leading_query(tail);
221 if parens {
222 w.push_str("(");
223 }
224
225 w.push_str("SELECT ");
226 w.write_expr(&self.select);
227 w.write_if(!self.from.is_empty(), " FROM ", &self.from, "");
228 w.write_if(!self.where_.is_empty(), " ", &self.where_, "");
229 w.write_if(!self.group_by.is_empty(), " ", &self.group_by, "");
230 w.write_if(!self.having.is_empty(), " ", &self.having, "");
231 w.write_if(!self.windows.is_empty(), " ", &self.windows, "");
232 w.write_if(!self.order_by.is_empty(), " ", &self.order_by, "");
233 w.write_if(!self.limit.is_empty(), " ", &self.limit, "");
234 w.write_if(!self.locks.is_empty(), " ", &self.locks, "");
235
236 if parens {
237 w.push_str(")");
238 }
239 w.write_if(!self.combines.is_empty(), " ", &self.combines, "");
240 }
241 }
242
243 fn from(table: &'static str) -> TableRef {
244 TableRef::new(quote(table))
245 }
246
247 #[test]
248 fn an_all_default_select_is_the_shortest_legal_statement() {
249 // Every clause absent contributes nothing, so only the projection's `*`
250 // survives. This is the property the whole module is built around.
251 //
252 // Not judged: `SELECT *` is as short as the clauses can make a statement,
253 // and PostgreSQL rejects it — `*` needs something to expand against
254 // ("SELECT * with no tables specified is not valid"). So what an
255 // all-default `Select` renders is the assertion, and the case below is
256 // where the same composition is judged as SQL.
257 let (sql, args) = build(&Numbered, &Select::default()).unwrap();
258 assert_eq!(sql, "SELECT *");
259 assert!(args.is_empty());
260 }
261
262 #[test]
263 fn a_select_over_a_cte_joined_and_filtered() {
264 // Expectation from the PostgreSQL 17 grammar:
265 // WITH with_query [, ...] SELECT select_list FROM from_item
266 // with_query: name [(col, ...)] AS (select)
267 // from_item: table_name [[AS] alias] join_type from_item USING (col)
268 let inner = Select {
269 select: SelectList {
270 columns: vec![quote("id")],
271 ..SelectList::default()
272 },
273 from: from("posts"),
274 ..Select::default()
275 };
276
277 let mut with = With::default();
278 with.append_cte(Cte {
279 columns: vec!["id".into()],
280 ..Cte::new("recent", Expr::custom(inner))
281 });
282
283 let mut from_users = from("users");
284 from_users.set_alias("u");
285 from_users.append_join(Join {
286 kind: JoinKind::Left,
287 to: from("recent"),
288 using: vec!["id".into()],
289 ..Join::default()
290 });
291
292 let q = Select {
293 with,
294 select: SelectList {
295 columns: vec![quote(("u", "id"))],
296 ..SelectList::default()
297 },
298 from: from_users,
299 where_: Where {
300 conditions: vec![quote(("u", "id")).eq(arg(7i32))],
301 },
302 ..Select::default()
303 };
304
305 let (sql, args) = build(&Numbered, &q).unwrap();
306 assert_stmt_sql(
307 &sql,
308 concat!(
309 r#"WITH "recent" ("id") AS (SELECT "id" FROM "posts") "#,
310 r#"SELECT "u"."id" FROM "users" AS "u" LEFT JOIN "recent" USING ("id") "#,
311 r#"WHERE ("u"."id" = $1)"#
312 ),
313 );
314 assert_eq!(args, vec![Value::I32(7)]);
315 }
316
317 #[test]
318 fn a_combined_select_keeps_its_own_limit_inside_the_parentheses() {
319 // PostgreSQL 17, sql-select: "If ORDER BY / LIMIT is to apply to only one
320 // of the operands, that operand must be parenthesised", and the trailing
321 // ORDER BY / LIMIT belong to the whole set operation.
322 let mut combines = Combines::default();
323 combines.append_combine(Combine {
324 op: Some(SetOp::Union),
325 query: Some(Expr::raw("SELECT 2")),
326 all: true,
327 });
328 combines.order_by.append_order(Expr::raw("1"));
329 combines.limit.set_limit(5);
330
331 let mut limit = Limit::default();
332 limit.set_limit(1);
333
334 let q = Select {
335 select: SelectList {
336 columns: vec![quote("id")],
337 ..SelectList::default()
338 },
339 from: from("users"),
340 limit,
341 combines,
342 ..Select::default()
343 };
344
345 assert_stmt_sql(
346 &build(&Numbered, &q).unwrap().0,
347 r#"(SELECT "id" FROM "users" LIMIT 1) UNION ALL (SELECT 2) ORDER BY 1 LIMIT 5"#,
348 );
349 }
350
351 #[test]
352 fn the_same_where_mod_reaches_a_statement_and_a_conflict_clause() {
353 // The point of the Has* traits: one function, three receivers.
354 // Qualified, because in an `ON CONFLICT DO UPDATE ... WHERE` an
355 // unqualified column is ambiguous between the target row and EXCLUDED —
356 // and a mod written once has to be legal in every receiver.
357 fn recent<Q: HasWhere>(q: &mut Q) {
358 q.where_mut().append_where(Expr::raw(r#""users"."id" > 1"#));
359 }
360
361 let mut select = Select {
362 from: from("users"),
363 ..Select::default()
364 };
365 recent(&mut select.where_);
366 assert_stmt_sql(
367 &build(&Numbered, &select).unwrap().0,
368 r#"SELECT * FROM "users" WHERE "users"."id" > 1"#,
369 );
370
371 // The target needs its column list for the predicate to attach to; that is
372 // the grammar, not this test's convenience.
373 let mut conflict = ConflictClause {
374 target: ConflictTarget::on_columns(quote("id")),
375 ..ConflictClause::do_update()
376 };
377 conflict
378 .set
379 .append_set(Expr::raw(r#""name" = EXCLUDED."name""#));
380 recent(&mut conflict);
381 recent(&mut conflict.target);
382 // The action's WHERE is framed; the *target's* is an index predicate, and
383 // the shared schema has no partial unique index for it to match — see
384 // `conflict::tests::a_partial_index_target_carries_the_indexs_own_predicate`.
385 // So this one pins the rendering and the framed case below pins the SQL.
386 assert_eq!(
387 build(&Numbered, &conflict).unwrap().0,
388 concat!(
389 r#"ON CONFLICT ("id") WHERE "users"."id" > 1 "#,
390 r#"DO UPDATE SET "name" = EXCLUDED."name" WHERE "users"."id" > 1"#
391 )
392 );
393
394 let mut action_only = ConflictClause {
395 target: ConflictTarget::on_columns(quote("id")),
396 ..ConflictClause::do_update()
397 };
398 action_only
399 .set
400 .append_set(Expr::raw(r#""name" = EXCLUDED."name""#));
401 recent(&mut action_only);
402 assert_frag_sql(
403 r#"INSERT INTO users ("id", "name") VALUES (1, 'kubo') {}"#,
404 &build(&Numbered, &action_only).unwrap().0,
405 r#"ON CONFLICT ("id") DO UPDATE SET "name" = EXCLUDED."name" WHERE "users"."id" > 1"#,
406 );
407 }
408
409 /// `write_quoted_list` is the shared helper every identifier list goes
410 /// through, so its empty case is load-bearing in six clauses.
411 ///
412 /// An identifier list is not a statement and belongs to no single one of them —
413 /// six clauses put it in six places — so this is one of the cases that stays a
414 /// string comparison.
415 #[test]
416 fn a_quoted_list_omits_its_affixes_when_empty() {
417 let mut w = SqlWriter::new(&Numbered);
418 write_quoted_list(&mut w, &[], " (", ", ", ")");
419 assert_eq!(w.sql(), "");
420 write_quoted_list(&mut w, &["a".into(), "b".into()], " (", ", ", ")");
421 assert_eq!(w.sql(), r#" ("a", "b")"#);
422 }
423}