Skip to main content

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}