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}