keelson-sqlite 0.1.0

The SQLite dialect for keelson.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
use std::borrow::Cow;

use keelson_core::clause::ConflictClause;
use keelson_core::expr::{Expr, IntoExpr};
use keelson_core::{Error, Expression, Query, SqlWriter};

// ---------------------------------------------------------------------------
// OR <conflict-algorithm>
// ---------------------------------------------------------------------------

/// The `conflict-clause` of an `INSERT` or `UPDATE`: `INSERT OR REPLACE INTO …`,
/// `UPDATE OR IGNORE …`.
///
/// From <https://www.sqlite.org/lang_insert.html> and
/// <https://www.sqlite.org/lang_update.html>:
///
/// ```text
/// INSERT OR { ROLLBACK | ABORT | REPLACE | FAIL | IGNORE } INTO …
/// UPDATE OR { ROLLBACK | ABORT | REPLACE | FAIL | IGNORE } …
/// ```
///
/// `ABORT` is the default and there is no reason to write it, but it *is* one of
/// the five keywords the grammar lists rather than an absence, so it is
/// representable — unlike PostgreSQL's `ALL` on a `SELECT`, which adds nothing at
/// all. A `DELETE` has no such clause, which is why nothing in
/// [`delete`](mod@crate::delete) mentions one.
///
/// SQLite's standalone `REPLACE INTO t …` is exactly `INSERT OR REPLACE INTO t …`;
/// only the longer spelling is produced, because the two are the same statement
/// and one spelling is enough.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Or {
    /// `OR ROLLBACK` — abort the whole transaction.
    Rollback,
    /// `OR ABORT` — the default: abort this statement, keep the transaction.
    Abort,
    /// `OR REPLACE` — delete the rows that conflict, then insert.
    Replace,
    /// `OR FAIL` — stop, but keep the changes already made by this statement.
    Fail,
    /// `OR IGNORE` — skip the offending row and carry on.
    Ignore,
}

impl Or {
    /// The keyword, as written after `OR`.
    pub fn as_str(self) -> &'static str {
        match self {
            Or::Rollback => "ROLLBACK",
            Or::Abort => "ABORT",
            Or::Replace => "REPLACE",
            Or::Fail => "FAIL",
            Or::Ignore => "IGNORE",
        }
    }
}

/// A statement that takes a `conflict-clause`: an `INSERT` or an `UPDATE`.
///
/// A `DELETE` does not implement this, which is how "a delete cannot violate a
/// constraint" is said — `delete::or_replace` does not exist to be misapplied.
pub trait HasOr {
    /// The conflict algorithm to modify.
    fn or_mut(&mut self) -> &mut Option<Or>;
}

impl HasOr for Option<Or> {
    fn or_mut(&mut self) -> &mut Option<Or> {
        self
    }
}

// ---------------------------------------------------------------------------
// Compound SELECTs
// ---------------------------------------------------------------------------

/// A `compound-operator` — the four SQLite has, and no more.
///
/// From <https://www.sqlite.org/syntax/compound-operator.html>:
///
/// ```text
/// UNION | UNION ALL | INTERSECT | EXCEPT
/// ```
///
/// `ALL` belongs to the operator here rather than being a separate flag, because
/// SQLite offers it on `UNION` alone: `INTERSECT ALL` and `EXCEPT ALL` are
/// rejected by SQLite's own parser as well as by the engine. Folding it into the
/// enum is how "there is no `INTERSECT ALL`" is said in a way that cannot be
/// written by accident — the shape PostgreSQL needs, a `SetOp` plus an `all` flag,
/// would make it representable.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompoundOp {
    /// `UNION` — rows of either, duplicates removed.
    Union,
    /// `UNION ALL` — rows of either, duplicates kept.
    UnionAll,
    /// `INTERSECT` — rows of both.
    Intersect,
    /// `EXCEPT` — rows of the left that are not in the right.
    Except,
}

impl CompoundOp {
    /// The operator, as written.
    pub fn as_str(self) -> &'static str {
        match self {
            CompoundOp::Union => "UNION",
            CompoundOp::UnionAll => "UNION ALL",
            CompoundOp::Intersect => "INTERSECT",
            CompoundOp::Except => "EXCEPT",
        }
    }
}

/// One operand of a compound `SELECT`: `UNION ALL <select-core>`.
///
/// **The operand is not parenthesised**, and that is the whole reason this type
/// exists instead of [`Combine`](keelson_core::clause::Combine). SQLite's
/// `compound-select-stmt` is a sequence of bare `select-core`s:
///
/// ```text
/// select-core ( compound-operator select-core )*
/// ```
///
/// A parenthesised select is only a *table-or-subquery* in SQLite, never a
/// compound operand, so `(SELECT 1) UNION (SELECT 2)` is a syntax error — verified
/// against SQLite's own parser and against a real SQLite. PostgreSQL parenthesises
/// every operand so that one may carry its own `ORDER BY`/`LIMIT`; SQLite cannot
/// express that at all, and correspondingly has no need for the `_combined` mods
/// `keelson_psql` carries.
#[derive(Debug, Clone, Default)]
pub struct Compound {
    /// Which operator. `None` is how a default-constructed operand stays absent.
    pub op: Option<CompoundOp>,
    /// The operand, rendered bare.
    pub query: Option<Expr>,
}

impl Compound {
    /// A compound operand joined by `op`.
    pub fn new(op: CompoundOp, query: impl IntoExpr) -> Compound {
        Compound {
            op: Some(op),
            query: Some(query.into_expr()),
        }
    }

    /// Whether this operand is absent.
    pub fn is_empty(&self) -> bool {
        self.op.is_none() && self.query.is_none()
    }
}

impl Expression for Compound {
    fn write_sql(&self, w: &mut SqlWriter<'_>) {
        if self.is_empty() {
            return;
        }
        // Half-filled is a caller error rather than an absent clause, and there is
        // no rendering that could be right.
        let Some(op) = self.op else {
            w.record_error(Error::Incomplete("the operator of a compound SELECT"));
            return;
        };
        let Some(query) = &self.query else {
            w.record_error(Error::Incomplete("the operand of a compound SELECT"));
            return;
        };

        w.push_str(op.as_str());
        w.push_str(" ");
        w.write_expr(query);
    }
}

/// Every compound operand chained onto one `SELECT`.
///
/// Unlike [`Combines`](keelson_core::clause::Combines) this holds *only* the
/// operands. SQLite's `ORDER BY` and `LIMIT` sit after the last operand and always
/// belong to the whole compound — there is no way to give one operand its own —
/// so the statement's single `ORDER BY`/`LIMIT`/`OFFSET` is already the
/// combination's, and no second set is needed.
#[derive(Debug, Clone, Default)]
pub struct Compounds {
    /// The operands, applied left to right.
    pub operands: Vec<Compound>,
}

impl Compounds {
    /// Append one operand.
    pub fn append_compound(&mut self, compound: Compound) {
        self.operands.push(compound);
    }

    /// Whether nothing is compounded onto the statement.
    ///
    /// A list of nothing but *absent* operands counts as empty, so the enclosing
    /// statement does not write the separator in front of a clause that renders
    /// nothing. A half-filled operand is not absent — it records a failure instead.
    pub fn is_empty(&self) -> bool {
        self.operands.iter().all(Compound::is_empty)
    }
}

impl Expression for Compounds {
    fn write_sql(&self, w: &mut SqlWriter<'_>) {
        write_spaced(w, self.operands.iter().filter(|c| !c.is_empty()));
    }
}

/// A `SELECT` other `SELECT`s can be compounded onto.
pub trait HasCompounds {
    /// The compound operands to modify.
    fn compounds_mut(&mut self) -> &mut Compounds;
}

impl HasCompounds for Compounds {
    fn compounds_mut(&mut self) -> &mut Compounds {
        self
    }
}

/// An `INSERT`'s `upsert-clause` list.
///
/// SQLite 3.35 and later accept several, tried in order:
///
/// ```text
/// INSERT … ON CONFLICT (a) DO UPDATE SET … ON CONFLICT DO NOTHING
/// ```
///
/// with the rule that only the last may omit its conflict target. PostgreSQL has
/// exactly one `ON CONFLICT`, which is why
/// [`Conflict`](keelson_core::clause::Conflict) is a single slot and this is a list
/// instead. The clause itself is core's
/// [`ConflictClause`](keelson_core::clause::ConflictClause) — SQLite's
/// `ON CONFLICT (cols) [WHERE …] DO { NOTHING | UPDATE SET … [WHERE …] }` is that
/// shape exactly, minus the `ON CONSTRAINT` target, for which no mod is exported.
pub trait HasUpserts {
    /// The upsert clauses to modify.
    fn upserts_mut(&mut self) -> &mut Vec<ConflictClause>;
}

impl HasUpserts for Vec<ConflictClause> {
    fn upserts_mut(&mut self) -> &mut Vec<ConflictClause> {
        self
    }
}

/// Write a sequence of possibly-absent items, single-space separated, writing
/// nothing at all when every one of them is absent.
///
/// `SqlWriter::write_iter` cannot be used: it would put a separator either side of
/// an item that renders nothing. Core has this helper too, privately, for exactly
/// the same reason.
pub(crate) fn write_spaced<'a, E: Expression + 'a>(
    w: &mut SqlWriter<'_>,
    items: impl IntoIterator<Item = &'a E>,
) {
    let mut written = false;
    for item in items {
        if written {
            w.push_str(" ");
        }
        w.write_expr(item);
        written = true;
    }
}

// ---------------------------------------------------------------------------
// Sub-queries and upsert values
// ---------------------------------------------------------------------------

/// A whole query standing in an expression slot, rendered in **its own** dialect.
///
/// [`SqlWriter::write_with_dialect`] keeps one shared argument list and
/// placeholder counter, so a sub-query re-indexes into its container for free.
#[derive(Debug)]
struct QueryExpr<Q>(Q);

impl<Q: Query> Expression for QueryExpr<Q> {
    fn write_sql(&self, w: &mut SqlWriter<'_>) {
        w.write_with_dialect(self.0.dialect(), &self.0);
    }
}

/// A query as an expression, **not** parenthesised.
///
/// The form for slots that supply their own parentheses — a `WITH` body,
/// `INSERT … SELECT` — *and* for a compound operand, which in SQLite must have no
/// parentheses at all. Use [`subquery`] where the parentheses belong to the
/// sub-query itself, as in a `FROM` item or a scalar sub-expression.
pub fn query(q: impl Query + 'static) -> Expr {
    Expr::custom(QueryExpr(q))
}

/// A parenthesised sub-query: `(SELECT …)`.
///
/// What a `table-or-subquery` or a scalar sub-expression needs. Unlike PostgreSQL,
/// SQLite does not require an alias on a `FROM` sub-query.
pub fn subquery(q: impl Query + 'static) -> Expr {
    Expr::group(query(q))
}

/// `excluded."col"` — the row that would have been inserted, inside
/// `ON CONFLICT … DO UPDATE`.
///
/// SQLite spells the pseudo-table in lower case
/// (<https://www.sqlite.org/lang_upsert.html>); the name is not quoted, because
/// `"excluded"` would be read as an ordinary table name.
pub fn excluded(column: impl Into<Cow<'static, str>>) -> Expr {
    Expr::join_with("", (Expr::raw("excluded."), Expr::ident(column.into())))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Sqlite;
    use keelson_core::build;

    fn sql(e: impl Expression) -> String {
        build(&Sqlite, &e).expect("render").0
    }

    /// <https://www.sqlite.org/syntax/compound-operator.html>
    #[test]
    fn a_compound_operand_carries_its_operator_and_no_parentheses() {
        assert_eq!(
            sql(Compound::new(CompoundOp::UnionAll, Expr::raw("SELECT 1"))),
            "UNION ALL SELECT 1"
        );
        assert_eq!(
            sql(Compound::new(CompoundOp::Intersect, Expr::raw("SELECT 1"))),
            "INTERSECT SELECT 1"
        );
        assert_eq!(
            sql(Compound::new(CompoundOp::Except, Expr::raw("SELECT 1"))),
            "EXCEPT SELECT 1"
        );
        assert_eq!(
            sql(Compound::new(CompoundOp::Union, Expr::raw("SELECT 1"))),
            "UNION SELECT 1"
        );
    }

    #[test]
    fn an_absent_operand_takes_its_separator_with_it() {
        let mut cs = Compounds::default();
        assert!(cs.is_empty());
        assert_eq!(sql(Compounds::default()), "");

        cs.append_compound(Compound::default());
        assert!(
            cs.is_empty(),
            "a list of nothing but absent operands is an absent clause, or the \
             statement writes the separator in front of nothing"
        );

        cs.append_compound(Compound::new(CompoundOp::Union, Expr::raw("SELECT 1")));
        cs.append_compound(Compound::default());
        assert!(!cs.is_empty());
        assert_eq!(sql(cs), "UNION SELECT 1");
    }

    #[test]
    fn a_half_filled_operand_is_a_recorded_failure() {
        let no_op = Compound {
            query: Some(Expr::raw("SELECT 1")),
            ..Compound::default()
        };
        let err = build(&Sqlite, &no_op).unwrap_err();
        // The substrings name the SQL concepts (the missing half of a compound
        // SELECT), not the message wording.
        assert!(
            matches!(&err, Error::Incomplete(what) if what.contains("operator")),
            "got: {err}"
        );

        let no_query = Compound {
            op: Some(CompoundOp::Union),
            ..Compound::default()
        };
        let err = build(&Sqlite, &no_query).unwrap_err();
        assert!(
            matches!(&err, Error::Incomplete(what) if what.contains("operand")),
            "got: {err}"
        );
    }

    /// <https://www.sqlite.org/lang_upsert.html>: the pseudo-table is `excluded`,
    /// unquoted and lower case.
    #[test]
    fn excluded_qualifies_the_column_with_the_pseudo_table() {
        assert_eq!(sql(excluded("email")), r#"excluded."email""#);
    }

    #[test]
    fn every_conflict_algorithm_has_its_keyword() {
        assert_eq!(Or::Rollback.as_str(), "ROLLBACK");
        assert_eq!(Or::Abort.as_str(), "ABORT");
        assert_eq!(Or::Replace.as_str(), "REPLACE");
        assert_eq!(Or::Fail.as_str(), "FAIL");
        assert_eq!(Or::Ignore.as_str(), "IGNORE");
    }
}