Skip to main content

keelson_core/clause/
combine.rs

1use crate::error::Error;
2use crate::expr::{Expr, IntoExpr};
3use crate::writer::{Expression, SqlWriter};
4
5use super::fetch::Fetch;
6use super::limit::Limit;
7use super::offset::Offset;
8use super::order_by::OrderBy;
9use super::{MaybeAbsent, write_present};
10
11/// Every set operation chained onto one statement, **and the `ORDER BY` / `LIMIT`
12/// / `OFFSET` / `FETCH` that belong to the combination rather than to any one
13/// operand**.
14///
15/// That second half is the part worth getting right. PostgreSQL 17,
16/// <https://www.postgresql.org/docs/17/sql-select.html>:
17///
18/// > `select_statement UNION [ALL] select_statement`
19/// >
20/// > `ORDER BY` and `LIMIT` … can be attached to a subexpression if it is enclosed
21/// > in parentheses. Without parentheses, these clauses will be taken to apply to
22/// > the result of the `UNION`, not to its right-hand input expression.
23///
24/// So a statement with set operations has two sets of trailing clauses that render
25/// in different places, and the difference is invisible in the SQL text except
26/// through the parentheses:
27///
28/// ```text
29/// (SELECT … LIMIT 1) UNION ALL (SELECT …) ORDER BY 1 LIMIT 5
30///  ^ the leading query's own LIMIT           ^ the combination's
31/// ```
32///
33/// The leading query's clauses stay where the query writes them, and are wrapped;
34/// the combination's live here and are written after the last operand.
35/// [`parenthesises_leading_query`](Self::parenthesises_leading_query) is the
36/// condition for the wrapping.
37///
38/// No keyword of its own: every [`Combine`] starts with its operator.
39#[derive(Debug, Clone, Default)]
40pub struct Combines {
41    /// The operations, applied left to right.
42    pub queries: Vec<Combine>,
43    /// `ORDER BY` over the result of the combination.
44    pub order_by: OrderBy,
45    /// `LIMIT` over the result of the combination.
46    pub limit: Limit,
47    /// `OFFSET` over the result of the combination.
48    pub offset: Offset,
49    /// `FETCH` over the result of the combination.
50    pub fetch: Fetch,
51}
52
53impl Combines {
54    /// Append one set operation.
55    pub fn append_combine(&mut self, combine: Combine) {
56        self.queries.push(combine);
57    }
58
59    /// Whether anything at all is combined.
60    pub fn is_empty(&self) -> bool {
61        self.queries.is_empty()
62            && self.order_by.is_empty()
63            && self.limit.is_empty()
64            && self.offset.is_empty()
65            && self.fetch.is_empty()
66    }
67
68    /// Whether the statement in front of these operations has to be parenthesised.
69    ///
70    /// It does exactly when it carries a trailing clause of its own —
71    /// `ORDER BY`, `LIMIT`, `OFFSET`, `FETCH` or a locking clause — *and*
72    /// something is combined onto it, because without the parentheses that clause
73    /// would silently move to the whole combination. `leading_has_tail_clauses` is
74    /// what only the query type can know.
75    ///
76    /// With nothing combined the parentheses would be legal but pointless, so they
77    /// are left out; that is also why an all-default `Combines` changes nothing
78    /// about how a statement renders.
79    pub fn parenthesises_leading_query(&self, leading_has_tail_clauses: bool) -> bool {
80        !self.queries.is_empty() && leading_has_tail_clauses
81    }
82}
83
84impl Expression for Combines {
85    fn write_sql(&self, w: &mut SqlWriter<'_>) {
86        // A combined tail clause exists to apply to the result of a set
87        // operation; with no operation there is no such result, and rendering
88        // the clause anyway would put it after the query's own tail clauses —
89        // `LIMIT $1 ORDER BY 1` — which no grammar accepts. The caller reached
90        // for a `*_combined` mod on a query that combines nothing, and that is
91        // recorded rather than guessed at. (Mods apply in any order, so the
92        // operation may well arrive after its tail clauses; this is judged only
93        // at render time, when everything has been applied.)
94        if self.queries.is_empty() {
95            if self.is_empty() {
96                return;
97            }
98            let missing = if !self.order_by.is_empty() {
99                "the set operation its combined ORDER BY applies to"
100            } else if !self.limit.is_empty() {
101                "the set operation its combined LIMIT applies to"
102            } else if !self.offset.is_empty() {
103                "the set operation its combined OFFSET applies to"
104            } else {
105                "the set operation its combined FETCH applies to"
106            };
107            w.record_error(Error::Incomplete(missing));
108            return;
109        }
110
111        // `LIMIT` and `FETCH` are two spellings of one grammar production, so
112        // the combination cannot carry both. Never last-write-wins: mod
113        // application order must not change meaning.
114        if !self.limit.is_empty() && !self.fetch.is_empty() {
115            w.record_error(Error::conflicting_clauses("LIMIT", "FETCH"));
116            return;
117        }
118
119        // Each part supplies its own separator only when something precedes it.
120        let mut written = !self.queries.is_empty();
121        write_present(w, &self.queries, "", " ", "");
122
123        for (present, clause) in [
124            (!self.order_by.is_empty(), &self.order_by as &dyn Expression),
125            (!self.limit.is_empty(), &self.limit),
126            (!self.offset.is_empty(), &self.offset),
127            (!self.fetch.is_empty(), &self.fetch),
128        ] {
129            if !present {
130                continue;
131            }
132            if written {
133                w.push_str(" ");
134            }
135            w.write_expr(clause);
136            written = true;
137        }
138    }
139}
140
141/// A statement other statements can be combined onto.
142pub trait HasCombines {
143    /// The set operations to modify.
144    fn combines_mut(&mut self) -> &mut Combines;
145}
146
147impl HasCombines for Combines {
148    fn combines_mut(&mut self) -> &mut Combines {
149        self
150    }
151}
152
153/// `UNION [ALL] (<query>)`
154///
155/// The operand is always parenthesised. It does not have to be — `a UNION b UNION
156/// c` is legal and left-associative — but a parenthesised operand cannot be
157/// re-associated by a later `ORDER BY`, and it is the only way an operand that has
158/// its own `LIMIT` can be written at all.
159#[derive(Debug, Clone, Default)]
160pub struct Combine {
161    /// Which set operation. `None` is how a default-constructed `Combine` stays
162    /// absent.
163    pub op: Option<SetOp>,
164    /// The right-hand operand, rendered inside parentheses.
165    pub query: Option<Expr>,
166    /// `ALL`: keep duplicate rows instead of removing them.
167    pub all: bool,
168}
169
170impl Combine {
171    /// A set operation of `op` against `query`, without `ALL`.
172    pub fn new(op: SetOp, query: impl IntoExpr) -> Self {
173        Combine {
174            op: Some(op),
175            query: Some(query.into_expr()),
176            all: false,
177        }
178    }
179
180    /// Whether this operation is absent.
181    pub fn is_empty(&self) -> bool {
182        self.op.is_none() && self.query.is_none()
183    }
184}
185
186impl Expression for Combine {
187    fn write_sql(&self, w: &mut SqlWriter<'_>) {
188        if self.is_empty() {
189            return;
190        }
191
192        // Half-filled is a caller error rather than an absent clause, and there is
193        // no rendering that could be right, so it is recorded instead of guessed.
194        let Some(op) = &self.op else {
195            w.record_error(Error::Incomplete("the operator of a set operation"));
196            return;
197        };
198        let Some(query) = &self.query else {
199            w.record_error(Error::Incomplete("the query of a set operation"));
200            return;
201        };
202
203        w.push_str(op.as_str());
204        w.push_str(if self.all { " ALL (" } else { " (" });
205        w.write_expr(query);
206        w.push_str(")");
207    }
208}
209
210/// Which set operation combines two queries.
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum SetOp {
213    /// `UNION` — rows of either.
214    Union,
215    /// `INTERSECT` — rows of both. Binds tighter than `UNION` and `EXCEPT`.
216    Intersect,
217    /// `EXCEPT` — rows of the left that are not in the right.
218    Except,
219}
220
221impl SetOp {
222    /// The keyword, as written.
223    pub fn as_str(self) -> &'static str {
224        match self {
225            SetOp::Union => "UNION",
226            SetOp::Intersect => "INTERSECT",
227            SetOp::Except => "EXCEPT",
228        }
229    }
230}
231
232impl MaybeAbsent for Combine {
233    fn is_absent(&self) -> bool {
234        self.is_empty()
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use keelson_sqlcheck::testing::assert_frag_sql;
241
242    use super::*;
243    use crate::dialect::testing::Numbered;
244    use crate::expr::arg;
245    use crate::value::Value;
246    use crate::writer::build;
247
248    /// A set operation is a fragment: it needs the query it is combined *with*.
249    /// One output column, matching every operand below — a set operation whose
250    /// arms disagree on arity parses fine and is rejected by the engine, which is
251    /// exactly the class of error this frame lets through to the judge.
252    const FRAME: &str = r#"SELECT "id" FROM users {}"#;
253
254    /// A whole sub-query. Its placeholder is compared against a column so that
255    /// PostgreSQL can infer a type for it.
256    fn sub(v: i32) -> Expr {
257        Expr::join((Expr::raw(r#"SELECT "id" FROM posts WHERE "id" ="#), arg(v)))
258    }
259
260    fn sub_sql(n: usize) -> String {
261        format!(r#"SELECT "id" FROM posts WHERE "id" = ${n}"#)
262    }
263
264    fn sql(e: &impl Expression) -> String {
265        build(&Numbered, e).expect("render").0
266    }
267
268    #[test]
269    fn an_empty_combines_writes_nothing() {
270        assert_frag_sql(FRAME, &sql(&Combines::default()), "");
271        assert_frag_sql(FRAME, &sql(&Combine::default()), "");
272        assert!(Combines::default().is_empty());
273        assert!(Combine::default().is_empty());
274    }
275
276    #[test]
277    fn all_goes_between_the_operator_and_the_operand() {
278        let mut c = Combine::new(SetOp::Union, sub(1));
279        assert_frag_sql(FRAME, &sql(&c), &format!("UNION ({})", sub_sql(1)));
280
281        c.all = true;
282        assert_frag_sql(FRAME, &sql(&c), &format!("UNION ALL ({})", sub_sql(1)));
283    }
284
285    #[test]
286    fn every_operator_has_its_spelling() {
287        for (op, keyword) in [
288            (SetOp::Union, "UNION"),
289            (SetOp::Intersect, "INTERSECT"),
290            (SetOp::Except, "EXCEPT"),
291        ] {
292            assert_frag_sql(
293                FRAME,
294                &sql(&Combine::new(op, Expr::raw("SELECT 1"))),
295                &format!("{keyword} (SELECT 1)"),
296            );
297        }
298    }
299
300    #[test]
301    fn a_half_filled_combine_is_a_recorded_failure_not_a_broken_fragment() {
302        let no_op = Combine {
303            query: Some(sub(1)),
304            ..Combine::default()
305        };
306        let err = build(&Numbered, &no_op).unwrap_err();
307        // The substring names the SQL concept (the missing operator), not the
308        // message wording.
309        assert!(
310            matches!(&err, Error::Incomplete(what) if what.contains("operator")),
311            "got: {err}"
312        );
313
314        let no_query = Combine {
315            op: Some(SetOp::Except),
316            ..Combine::default()
317        };
318        let err = build(&Numbered, &no_query).unwrap_err();
319        // The substring names the SQL concept (the missing operand query), not
320        // the message wording.
321        assert!(
322            matches!(&err, Error::Incomplete(what) if what.contains("query")),
323            "got: {err}"
324        );
325    }
326
327    #[test]
328    fn chained_operations_keep_one_placeholder_run() {
329        let mut cs = Combines::default();
330        cs.append_combine(Combine::new(SetOp::Union, sub(1)));
331        cs.append_combine(Combine::new(SetOp::Intersect, sub(2)));
332
333        let (rendered, args) = build(&Numbered, &cs).unwrap();
334        assert_frag_sql(
335            FRAME,
336            &rendered,
337            &format!("UNION ({}) INTERSECT ({})", sub_sql(1), sub_sql(2)),
338        );
339        assert_eq!(args, vec![Value::I32(1), Value::I32(2)]);
340    }
341
342    #[test]
343    fn the_combinations_own_tail_clauses_follow_the_last_operand() {
344        // PostgreSQL 17: an unparenthesised trailing ORDER BY / LIMIT applies to
345        // the result of the UNION, which is exactly what these fields are for.
346        let mut cs = Combines::default();
347        cs.append_combine(Combine::new(SetOp::Union, sub(1)));
348        cs.order_by.append_order("1");
349        cs.limit.set_limit(10i64);
350        cs.offset.set_offset(5i64);
351
352        assert_frag_sql(
353            FRAME,
354            &sql(&cs),
355            &format!("UNION ({}) ORDER BY 1 LIMIT 10 OFFSET 5", sub_sql(1)),
356        );
357    }
358
359    #[test]
360    fn a_tail_clause_without_a_set_operation_is_a_recorded_failure() {
361        // A combined tail clause with nothing combined has no result to apply
362        // to, and rendering it would collide with the query's own tail clauses
363        // (`LIMIT $1 ORDER BY 1`). It still makes the `Combines` non-empty —
364        // that is what routes it into `write_sql`, where it is recorded.
365        let mut cs = Combines::default();
366        cs.fetch.set_fetch(2i64);
367        assert!(!cs.is_empty());
368        let err = build(&Numbered, &cs).unwrap_err();
369        // The substrings name the SQL concepts (the missing set operation and
370        // the clause left dangling), not the message wording.
371        assert!(
372            matches!(&err, Error::Incomplete(what)
373                if what.contains("set operation") && what.contains("FETCH")),
374            "got: {err}"
375        );
376
377        let mut cs = Combines::default();
378        cs.order_by.append_order("1");
379        let err = build(&Numbered, &cs).unwrap_err();
380        assert!(
381            matches!(&err, Error::Incomplete(what)
382                if what.contains("set operation") && what.contains("ORDER BY")),
383            "got: {err}"
384        );
385    }
386
387    #[test]
388    fn a_combined_limit_and_fetch_together_are_a_recorded_failure() {
389        // gram.y `select_limit`: LIMIT and FETCH are one production's two
390        // spellings, so the combination's tail cannot carry both — and which
391        // was applied last must not decide which wins.
392        let mut cs = Combines::default();
393        cs.append_combine(Combine::new(SetOp::Union, sub(1)));
394        cs.limit.set_limit(10i64);
395        cs.fetch.set_fetch(2i64);
396        let err = build(&Numbered, &cs).unwrap_err();
397        assert!(
398            matches!(
399                &err,
400                Error::ConflictingClauses {
401                    first: "LIMIT",
402                    second: "FETCH"
403                }
404            ),
405            "got: {err}"
406        );
407    }
408
409    #[test]
410    fn the_leading_query_is_wrapped_only_when_both_conditions_hold() {
411        let mut cs = Combines::default();
412        assert!(
413            !cs.parenthesises_leading_query(true),
414            "nothing combined: the parentheses would say nothing"
415        );
416
417        cs.append_combine(Combine::new(SetOp::Union, sub(1)));
418        assert!(
419            !cs.parenthesises_leading_query(false),
420            "no tail clause on the leading query: nothing to protect"
421        );
422        assert!(cs.parenthesises_leading_query(true));
423    }
424}