Skip to main content

keelson_sqlite/
extras.rs

1use std::borrow::Cow;
2
3use keelson_core::clause::ConflictClause;
4use keelson_core::expr::{Expr, IntoExpr};
5use keelson_core::{Error, Expression, Query, SqlWriter};
6
7// ---------------------------------------------------------------------------
8// OR <conflict-algorithm>
9// ---------------------------------------------------------------------------
10
11/// The `conflict-clause` of an `INSERT` or `UPDATE`: `INSERT OR REPLACE INTO …`,
12/// `UPDATE OR IGNORE …`.
13///
14/// From <https://www.sqlite.org/lang_insert.html> and
15/// <https://www.sqlite.org/lang_update.html>:
16///
17/// ```text
18/// INSERT OR { ROLLBACK | ABORT | REPLACE | FAIL | IGNORE } INTO …
19/// UPDATE OR { ROLLBACK | ABORT | REPLACE | FAIL | IGNORE } …
20/// ```
21///
22/// `ABORT` is the default and there is no reason to write it, but it *is* one of
23/// the five keywords the grammar lists rather than an absence, so it is
24/// representable — unlike PostgreSQL's `ALL` on a `SELECT`, which adds nothing at
25/// all. A `DELETE` has no such clause, which is why nothing in
26/// [`delete`](mod@crate::delete) mentions one.
27///
28/// SQLite's standalone `REPLACE INTO t …` is exactly `INSERT OR REPLACE INTO t …`;
29/// only the longer spelling is produced, because the two are the same statement
30/// and one spelling is enough.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Or {
33    /// `OR ROLLBACK` — abort the whole transaction.
34    Rollback,
35    /// `OR ABORT` — the default: abort this statement, keep the transaction.
36    Abort,
37    /// `OR REPLACE` — delete the rows that conflict, then insert.
38    Replace,
39    /// `OR FAIL` — stop, but keep the changes already made by this statement.
40    Fail,
41    /// `OR IGNORE` — skip the offending row and carry on.
42    Ignore,
43}
44
45impl Or {
46    /// The keyword, as written after `OR`.
47    pub fn as_str(self) -> &'static str {
48        match self {
49            Or::Rollback => "ROLLBACK",
50            Or::Abort => "ABORT",
51            Or::Replace => "REPLACE",
52            Or::Fail => "FAIL",
53            Or::Ignore => "IGNORE",
54        }
55    }
56}
57
58/// A statement that takes a `conflict-clause`: an `INSERT` or an `UPDATE`.
59///
60/// A `DELETE` does not implement this, which is how "a delete cannot violate a
61/// constraint" is said — `delete::or_replace` does not exist to be misapplied.
62pub trait HasOr {
63    /// The conflict algorithm to modify.
64    fn or_mut(&mut self) -> &mut Option<Or>;
65}
66
67impl HasOr for Option<Or> {
68    fn or_mut(&mut self) -> &mut Option<Or> {
69        self
70    }
71}
72
73// ---------------------------------------------------------------------------
74// Compound SELECTs
75// ---------------------------------------------------------------------------
76
77/// A `compound-operator` — the four SQLite has, and no more.
78///
79/// From <https://www.sqlite.org/syntax/compound-operator.html>:
80///
81/// ```text
82/// UNION | UNION ALL | INTERSECT | EXCEPT
83/// ```
84///
85/// `ALL` belongs to the operator here rather than being a separate flag, because
86/// SQLite offers it on `UNION` alone: `INTERSECT ALL` and `EXCEPT ALL` are
87/// rejected by SQLite's own parser as well as by the engine. Folding it into the
88/// enum is how "there is no `INTERSECT ALL`" is said in a way that cannot be
89/// written by accident — the shape PostgreSQL needs, a `SetOp` plus an `all` flag,
90/// would make it representable.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum CompoundOp {
93    /// `UNION` — rows of either, duplicates removed.
94    Union,
95    /// `UNION ALL` — rows of either, duplicates kept.
96    UnionAll,
97    /// `INTERSECT` — rows of both.
98    Intersect,
99    /// `EXCEPT` — rows of the left that are not in the right.
100    Except,
101}
102
103impl CompoundOp {
104    /// The operator, as written.
105    pub fn as_str(self) -> &'static str {
106        match self {
107            CompoundOp::Union => "UNION",
108            CompoundOp::UnionAll => "UNION ALL",
109            CompoundOp::Intersect => "INTERSECT",
110            CompoundOp::Except => "EXCEPT",
111        }
112    }
113}
114
115/// One operand of a compound `SELECT`: `UNION ALL <select-core>`.
116///
117/// **The operand is not parenthesised**, and that is the whole reason this type
118/// exists instead of [`Combine`](keelson_core::clause::Combine). SQLite's
119/// `compound-select-stmt` is a sequence of bare `select-core`s:
120///
121/// ```text
122/// select-core ( compound-operator select-core )*
123/// ```
124///
125/// A parenthesised select is only a *table-or-subquery* in SQLite, never a
126/// compound operand, so `(SELECT 1) UNION (SELECT 2)` is a syntax error — verified
127/// against SQLite's own parser and against a real SQLite. PostgreSQL parenthesises
128/// every operand so that one may carry its own `ORDER BY`/`LIMIT`; SQLite cannot
129/// express that at all, and correspondingly has no need for the `_combined` mods
130/// `keelson_psql` carries.
131#[derive(Debug, Clone, Default)]
132pub struct Compound {
133    /// Which operator. `None` is how a default-constructed operand stays absent.
134    pub op: Option<CompoundOp>,
135    /// The operand, rendered bare.
136    pub query: Option<Expr>,
137}
138
139impl Compound {
140    /// A compound operand joined by `op`.
141    pub fn new(op: CompoundOp, query: impl IntoExpr) -> Compound {
142        Compound {
143            op: Some(op),
144            query: Some(query.into_expr()),
145        }
146    }
147
148    /// Whether this operand is absent.
149    pub fn is_empty(&self) -> bool {
150        self.op.is_none() && self.query.is_none()
151    }
152}
153
154impl Expression for Compound {
155    fn write_sql(&self, w: &mut SqlWriter<'_>) {
156        if self.is_empty() {
157            return;
158        }
159        // Half-filled is a caller error rather than an absent clause, and there is
160        // no rendering that could be right.
161        let Some(op) = self.op else {
162            w.record_error(Error::Incomplete("the operator of a compound SELECT"));
163            return;
164        };
165        let Some(query) = &self.query else {
166            w.record_error(Error::Incomplete("the operand of a compound SELECT"));
167            return;
168        };
169
170        w.push_str(op.as_str());
171        w.push_str(" ");
172        w.write_expr(query);
173    }
174}
175
176/// Every compound operand chained onto one `SELECT`.
177///
178/// Unlike [`Combines`](keelson_core::clause::Combines) this holds *only* the
179/// operands. SQLite's `ORDER BY` and `LIMIT` sit after the last operand and always
180/// belong to the whole compound — there is no way to give one operand its own —
181/// so the statement's single `ORDER BY`/`LIMIT`/`OFFSET` is already the
182/// combination's, and no second set is needed.
183#[derive(Debug, Clone, Default)]
184pub struct Compounds {
185    /// The operands, applied left to right.
186    pub operands: Vec<Compound>,
187}
188
189impl Compounds {
190    /// Append one operand.
191    pub fn append_compound(&mut self, compound: Compound) {
192        self.operands.push(compound);
193    }
194
195    /// Whether nothing is compounded onto the statement.
196    ///
197    /// A list of nothing but *absent* operands counts as empty, so the enclosing
198    /// statement does not write the separator in front of a clause that renders
199    /// nothing. A half-filled operand is not absent — it records a failure instead.
200    pub fn is_empty(&self) -> bool {
201        self.operands.iter().all(Compound::is_empty)
202    }
203}
204
205impl Expression for Compounds {
206    fn write_sql(&self, w: &mut SqlWriter<'_>) {
207        write_spaced(w, self.operands.iter().filter(|c| !c.is_empty()));
208    }
209}
210
211/// A `SELECT` other `SELECT`s can be compounded onto.
212pub trait HasCompounds {
213    /// The compound operands to modify.
214    fn compounds_mut(&mut self) -> &mut Compounds;
215}
216
217impl HasCompounds for Compounds {
218    fn compounds_mut(&mut self) -> &mut Compounds {
219        self
220    }
221}
222
223/// An `INSERT`'s `upsert-clause` list.
224///
225/// SQLite 3.35 and later accept several, tried in order:
226///
227/// ```text
228/// INSERT … ON CONFLICT (a) DO UPDATE SET … ON CONFLICT DO NOTHING
229/// ```
230///
231/// with the rule that only the last may omit its conflict target. PostgreSQL has
232/// exactly one `ON CONFLICT`, which is why
233/// [`Conflict`](keelson_core::clause::Conflict) is a single slot and this is a list
234/// instead. The clause itself is core's
235/// [`ConflictClause`](keelson_core::clause::ConflictClause) — SQLite's
236/// `ON CONFLICT (cols) [WHERE …] DO { NOTHING | UPDATE SET … [WHERE …] }` is that
237/// shape exactly, minus the `ON CONSTRAINT` target, for which no mod is exported.
238pub trait HasUpserts {
239    /// The upsert clauses to modify.
240    fn upserts_mut(&mut self) -> &mut Vec<ConflictClause>;
241}
242
243impl HasUpserts for Vec<ConflictClause> {
244    fn upserts_mut(&mut self) -> &mut Vec<ConflictClause> {
245        self
246    }
247}
248
249/// Write a sequence of possibly-absent items, single-space separated, writing
250/// nothing at all when every one of them is absent.
251///
252/// `SqlWriter::write_iter` cannot be used: it would put a separator either side of
253/// an item that renders nothing. Core has this helper too, privately, for exactly
254/// the same reason.
255pub(crate) fn write_spaced<'a, E: Expression + 'a>(
256    w: &mut SqlWriter<'_>,
257    items: impl IntoIterator<Item = &'a E>,
258) {
259    let mut written = false;
260    for item in items {
261        if written {
262            w.push_str(" ");
263        }
264        w.write_expr(item);
265        written = true;
266    }
267}
268
269// ---------------------------------------------------------------------------
270// Sub-queries and upsert values
271// ---------------------------------------------------------------------------
272
273/// A whole query standing in an expression slot, rendered in **its own** dialect.
274///
275/// [`SqlWriter::write_with_dialect`] keeps one shared argument list and
276/// placeholder counter, so a sub-query re-indexes into its container for free.
277#[derive(Debug)]
278struct QueryExpr<Q>(Q);
279
280impl<Q: Query> Expression for QueryExpr<Q> {
281    fn write_sql(&self, w: &mut SqlWriter<'_>) {
282        w.write_with_dialect(self.0.dialect(), &self.0);
283    }
284}
285
286/// A query as an expression, **not** parenthesised.
287///
288/// The form for slots that supply their own parentheses — a `WITH` body,
289/// `INSERT … SELECT` — *and* for a compound operand, which in SQLite must have no
290/// parentheses at all. Use [`subquery`] where the parentheses belong to the
291/// sub-query itself, as in a `FROM` item or a scalar sub-expression.
292pub fn query(q: impl Query + 'static) -> Expr {
293    Expr::custom(QueryExpr(q))
294}
295
296/// A parenthesised sub-query: `(SELECT …)`.
297///
298/// What a `table-or-subquery` or a scalar sub-expression needs. Unlike PostgreSQL,
299/// SQLite does not require an alias on a `FROM` sub-query.
300pub fn subquery(q: impl Query + 'static) -> Expr {
301    Expr::group(query(q))
302}
303
304/// `excluded."col"` — the row that would have been inserted, inside
305/// `ON CONFLICT … DO UPDATE`.
306///
307/// SQLite spells the pseudo-table in lower case
308/// (<https://www.sqlite.org/lang_upsert.html>); the name is not quoted, because
309/// `"excluded"` would be read as an ordinary table name.
310pub fn excluded(column: impl Into<Cow<'static, str>>) -> Expr {
311    Expr::join_with("", (Expr::raw("excluded."), Expr::ident(column.into())))
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::Sqlite;
318    use keelson_core::build;
319
320    fn sql(e: impl Expression) -> String {
321        build(&Sqlite, &e).expect("render").0
322    }
323
324    /// <https://www.sqlite.org/syntax/compound-operator.html>
325    #[test]
326    fn a_compound_operand_carries_its_operator_and_no_parentheses() {
327        assert_eq!(
328            sql(Compound::new(CompoundOp::UnionAll, Expr::raw("SELECT 1"))),
329            "UNION ALL SELECT 1"
330        );
331        assert_eq!(
332            sql(Compound::new(CompoundOp::Intersect, Expr::raw("SELECT 1"))),
333            "INTERSECT SELECT 1"
334        );
335        assert_eq!(
336            sql(Compound::new(CompoundOp::Except, Expr::raw("SELECT 1"))),
337            "EXCEPT SELECT 1"
338        );
339        assert_eq!(
340            sql(Compound::new(CompoundOp::Union, Expr::raw("SELECT 1"))),
341            "UNION SELECT 1"
342        );
343    }
344
345    #[test]
346    fn an_absent_operand_takes_its_separator_with_it() {
347        let mut cs = Compounds::default();
348        assert!(cs.is_empty());
349        assert_eq!(sql(Compounds::default()), "");
350
351        cs.append_compound(Compound::default());
352        assert!(
353            cs.is_empty(),
354            "a list of nothing but absent operands is an absent clause, or the \
355             statement writes the separator in front of nothing"
356        );
357
358        cs.append_compound(Compound::new(CompoundOp::Union, Expr::raw("SELECT 1")));
359        cs.append_compound(Compound::default());
360        assert!(!cs.is_empty());
361        assert_eq!(sql(cs), "UNION SELECT 1");
362    }
363
364    #[test]
365    fn a_half_filled_operand_is_a_recorded_failure() {
366        let no_op = Compound {
367            query: Some(Expr::raw("SELECT 1")),
368            ..Compound::default()
369        };
370        let err = build(&Sqlite, &no_op).unwrap_err();
371        // The substrings name the SQL concepts (the missing half of a compound
372        // SELECT), not the message wording.
373        assert!(
374            matches!(&err, Error::Incomplete(what) if what.contains("operator")),
375            "got: {err}"
376        );
377
378        let no_query = Compound {
379            op: Some(CompoundOp::Union),
380            ..Compound::default()
381        };
382        let err = build(&Sqlite, &no_query).unwrap_err();
383        assert!(
384            matches!(&err, Error::Incomplete(what) if what.contains("operand")),
385            "got: {err}"
386        );
387    }
388
389    /// <https://www.sqlite.org/lang_upsert.html>: the pseudo-table is `excluded`,
390    /// unquoted and lower case.
391    #[test]
392    fn excluded_qualifies_the_column_with_the_pseudo_table() {
393        assert_eq!(sql(excluded("email")), r#"excluded."email""#);
394    }
395
396    #[test]
397    fn every_conflict_algorithm_has_its_keyword() {
398        assert_eq!(Or::Rollback.as_str(), "ROLLBACK");
399        assert_eq!(Or::Abort.as_str(), "ABORT");
400        assert_eq!(Or::Replace.as_str(), "REPLACE");
401        assert_eq!(Or::Fail.as_str(), "FAIL");
402        assert_eq!(Or::Ignore.as_str(), "IGNORE");
403    }
404}