Skip to main content

turso_sql/
writer.rs

1//! Rendering of the AST into SQL text and bound values, modeled by [`Statement`].
2//!
3//! The writer is the single place that knows how the Turso dialect is
4//! spelled: identifier quoting, placeholder syntax, where parentheses are
5//! required and which clauses can carry parameters. Keeping that knowledge
6//! in one private type lets every builder stay a plain data structure and
7//! makes the output deterministic — the same tree always renders the same
8//! text, which is what makes the engine's prepared-statement cache useful.
9//!
10//! Values are bound as `?` parameters wherever SQLite allows it, including
11//! `LIMIT` and `OFFSET`. The exceptions are `DEFAULT` clauses in DDL, which
12//! SQLite requires to be literal, and `UPDATE ... LIMIT`, which is written
13//! inline; both are rendered through [`Value::to_literal`].
14//!
15//! - [`Statement`]: the rendered output, SQL plus values in placeholder
16//!   order;
17//! - [`Build`]: implemented by every statement and expression type so that
18//!   `.build()` and `.to_statement()` work uniformly.
19
20use std::fmt::Write as _;
21
22use crate::expr::{Condition, Expr, Func, Order};
23use crate::iden::{ColumnRef, Ident, TableRef};
24use crate::query::{
25    ConflictAction, Delete, Insert, JoinType, Returning, Select, SelectItem, Update,
26};
27use crate::schema::{
28    AlterOp, AlterTable, ColumnDef, CreateIndex, CreateTable, DropIndex, DropTable, TableConstraint,
29};
30use crate::value::Value;
31
32/// A rendered statement: SQL with `?` placeholders and the values to bind.
33#[derive(Clone, Debug, PartialEq)]
34pub struct Statement {
35    /// The SQL text.
36    pub sql: String,
37    /// The bound values, in placeholder order.
38    pub values: Vec<Value>,
39}
40
41impl Statement {
42    /// Wraps raw SQL without parameters.
43    pub fn from_string(sql: impl Into<String>) -> Self {
44        Self {
45            sql: sql.into(),
46            values: Vec::new(),
47        }
48    }
49
50    /// Wraps raw SQL with `?` placeholders and the values that fill them.
51    pub fn from_sql_and_values<V: Into<Value>>(
52        sql: impl Into<String>,
53        values: impl IntoIterator<Item = V>,
54    ) -> Self {
55        Self {
56            sql: sql.into(),
57            values: values.into_iter().map(Into::into).collect(),
58        }
59    }
60
61    /// The SQL with literals inlined, for logs and tests only.
62    ///
63    /// The substitution is a plain scan for `?`, so a question mark inside a
64    /// string literal of the SQL would be replaced too; that is acceptable
65    /// for a debugging aid and is why this text is never executed.
66    pub fn to_string_inlined(&self) -> String {
67        let mut out = String::with_capacity(self.sql.len() + 16);
68        let mut values = self.values.iter();
69        for ch in self.sql.chars() {
70            if ch == '?' {
71                match values.next() {
72                    Some(v) => out.push_str(&v.to_literal()),
73                    None => out.push('?'),
74                }
75            } else {
76                out.push(ch);
77            }
78        }
79        out
80    }
81}
82
83impl std::fmt::Display for Statement {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.write_str(&self.to_string_inlined())
86    }
87}
88
89/// Anything that renders to a [`Statement`].
90pub trait Build {
91    /// Renders to the SQL text and the values to bind.
92    fn build(&self) -> (String, Vec<Value>) {
93        let stmt = self.to_statement();
94        (stmt.sql, stmt.values)
95    }
96
97    /// Renders into a [`Statement`].
98    fn to_statement(&self) -> Statement;
99
100    /// Renders with literals inlined, for logs and tests only.
101    fn to_string_inlined(&self) -> String {
102        self.to_statement().to_string_inlined()
103    }
104}
105
106/// The rendering state: the SQL text being built and the values bound so
107/// far, in placeholder order.
108#[derive(Default)]
109struct Writer {
110    /// The SQL text.
111    sql: String,
112    /// The bound values, in the order their `?` was written.
113    values: Vec<Value>,
114}
115
116impl Writer {
117    /// Appends raw text.
118    fn push(&mut self, s: &str) {
119        self.sql.push_str(s);
120    }
121
122    /// Appends a double-quoted identifier, doubling embedded quotes.
123    fn ident(&mut self, ident: &Ident) {
124        self.sql.push('"');
125        self.sql.push_str(&ident.name().replace('"', "\"\""));
126        self.sql.push('"');
127    }
128
129    /// Appends a `?` placeholder and records its value.
130    fn param(&mut self, value: Value) {
131        self.sql.push('?');
132        self.values.push(value);
133    }
134
135    /// Appends a table reference with its alias.
136    fn table_ref(&mut self, t: &TableRef) {
137        self.ident(&t.name);
138        if let Some(alias) = &t.alias {
139            self.push(" AS ");
140            self.ident(alias);
141        }
142    }
143
144    /// Appends a column reference.
145    fn column_ref(&mut self, c: &ColumnRef) {
146        match c {
147            ColumnRef::Column(name) => self.ident(name),
148            ColumnRef::TableColumn(table, name) => {
149                self.ident(table);
150                self.push(".");
151                self.ident(name);
152            }
153            ColumnRef::Asterisk => self.push("*"),
154            ColumnRef::TableAsterisk(table) => {
155                self.ident(table);
156                self.push(".*");
157            }
158        }
159    }
160
161    /// Appends `items` separated by `, `, rendering each with `f`.
162    fn list<T>(&mut self, items: &[T], mut f: impl FnMut(&mut Self, &T)) {
163        for (i, item) in items.iter().enumerate() {
164            if i > 0 {
165                self.push(", ");
166            }
167            f(self, item);
168        }
169    }
170
171    /// Appends an expression.
172    #[allow(clippy::too_many_lines, reason = "one arm per Expr variant")]
173    fn expr(&mut self, e: &Expr) {
174        match e {
175            Expr::Column(c) => self.column_ref(c),
176            Expr::Value(v) => self.param(v.clone()),
177            Expr::Tuple(items) => {
178                self.push("(");
179                self.list(items, Self::expr);
180                self.push(")");
181            }
182            Expr::Binary(l, op, r) => {
183                self.operand(l);
184                self.push(" ");
185                self.push(op.sql());
186                self.push(" ");
187                self.operand(r);
188            }
189            Expr::Not(inner) => {
190                self.push("NOT ");
191                self.operand(inner);
192            }
193            Expr::Neg(inner) => {
194                self.push("-");
195                self.operand(inner);
196            }
197            Expr::IsNull(inner) => {
198                self.operand(inner);
199                self.push(" IS NULL");
200            }
201            Expr::IsNotNull(inner) => {
202                self.operand(inner);
203                self.push(" IS NOT NULL");
204            }
205            Expr::In(l, r) => {
206                self.operand(l);
207                self.push(" IN ");
208                self.in_rhs(r);
209            }
210            Expr::NotIn(l, r) => {
211                self.operand(l);
212                self.push(" NOT IN ");
213                self.in_rhs(r);
214            }
215            Expr::Between(x, a, b) => {
216                self.operand(x);
217                self.push(" BETWEEN ");
218                self.operand(a);
219                self.push(" AND ");
220                self.operand(b);
221            }
222            Expr::NotBetween(x, a, b) => {
223                self.operand(x);
224                self.push(" NOT BETWEEN ");
225                self.operand(a);
226                self.push(" AND ");
227                self.operand(b);
228            }
229            Expr::Like {
230                expr,
231                pattern,
232                negated,
233                escape,
234            } => {
235                self.operand(expr);
236                self.push(if *negated { " NOT LIKE " } else { " LIKE " });
237                self.operand(pattern);
238                if let Some(c) = escape {
239                    // The escape character is bound rather than inlined so
240                    // that a quote never has to be doubled by hand.
241                    self.push(" ESCAPE ");
242                    self.param(Value::Text(c.to_string()));
243                }
244            }
245            Expr::Func(f) => self.func(f),
246            Expr::Subquery(s) => {
247                self.push("(");
248                self.select(s);
249                self.push(")");
250            }
251            Expr::Exists(s) => {
252                self.push("EXISTS (");
253                self.select(s);
254                self.push(")");
255            }
256            Expr::Case(whens, otherwise) => {
257                self.push("CASE");
258                for (cond, then) in whens {
259                    self.push(" WHEN ");
260                    self.expr(cond);
261                    self.push(" THEN ");
262                    self.expr(then);
263                }
264                if let Some(o) = otherwise {
265                    self.push(" ELSE ");
266                    self.expr(o);
267                }
268                self.push(" END");
269            }
270            Expr::Cast(inner, ty) => {
271                self.push("CAST(");
272                self.expr(inner);
273                self.push(" AS ");
274                self.push(ty);
275                self.push(")");
276            }
277            Expr::Alias(inner, alias) => {
278                self.expr(inner);
279                self.push(" AS ");
280                self.ident(alias);
281            }
282            Expr::Raw(sql, values) => {
283                self.push(sql);
284                self.values.extend(values.iter().cloned());
285            }
286            Expr::Paren(inner) => {
287                self.push("(");
288                self.expr(inner);
289                self.push(")");
290            }
291        }
292    }
293
294    /// Appends a sub-expression, parenthesising compound operators.
295    ///
296    /// Wrapping every nested binary, `NOT` and `BETWEEN` node is more
297    /// parentheses than strictly needed, but it means the writer never has
298    /// to encode SQL precedence tables and the output is always unambiguous.
299    fn operand(&mut self, e: &Expr) {
300        match e {
301            Expr::Binary(..)
302            | Expr::Not(_)
303            | Expr::Between(..)
304            | Expr::NotBetween(..)
305            | Expr::Like { .. } => {
306                self.push("(");
307                self.expr(e);
308                self.push(")");
309            }
310            _ => self.expr(e),
311        }
312    }
313
314    /// Appends the right-hand side of `IN`.
315    ///
316    /// SQLite rejects `IN ()`, so an empty tuple is rendered as `(NULL)`,
317    /// which matches no row and keeps a filter built from an empty list
318    /// well-formed.
319    fn in_rhs(&mut self, e: &Expr) {
320        match e {
321            Expr::Tuple(items) if items.is_empty() => self.push("(NULL)"),
322            Expr::Tuple(_) | Expr::Subquery(_) => self.expr(e),
323            other => {
324                self.push("(");
325                self.expr(other);
326                self.push(")");
327            }
328        }
329    }
330
331    /// Appends a function call.
332    fn func(&mut self, f: &Func) {
333        self.push(f.name);
334        self.push("(");
335        if f.distinct {
336            self.push("DISTINCT ");
337        }
338        self.list(&f.args, Self::expr);
339        self.push(")");
340    }
341
342    /// Appends `keyword` followed by the condition, or nothing when the
343    /// condition is empty.
344    fn condition(&mut self, keyword: &str, cond: &Condition) {
345        if let Some(expr) = cond.clone().into_expr() {
346            self.push(keyword);
347            self.expr(&expr);
348        }
349    }
350
351    /// Appends a `SELECT` statement.
352    fn select(&mut self, s: &Select) {
353        self.push("SELECT ");
354        if s.distinct {
355            self.push("DISTINCT ");
356        }
357        if s.items.is_empty() {
358            self.push("*");
359        } else {
360            self.list(&s.items, |w, item| match item {
361                SelectItem::Expr(e) => w.expr(e),
362            });
363        }
364        if !s.from.is_empty() {
365            self.push(" FROM ");
366            self.list(&s.from, Self::table_ref);
367        }
368        if let Some((sub, alias)) = &s.from_subquery {
369            self.push(if s.from.is_empty() { " FROM (" } else { ", (" });
370            self.select(sub);
371            self.push(") AS ");
372            self.ident(alias);
373        }
374        for join in &s.joins {
375            self.push(match join.kind {
376                JoinType::Inner => " INNER JOIN ",
377                JoinType::Left => " LEFT JOIN ",
378                JoinType::Cross => " CROSS JOIN ",
379            });
380            self.table_ref(&join.table);
381            if let Some(on) = &join.on {
382                self.push(" ON ");
383                self.expr(on);
384            }
385        }
386        self.condition(" WHERE ", &s.r#where);
387        if !s.group_by.is_empty() {
388            self.push(" GROUP BY ");
389            self.list(&s.group_by, Self::expr);
390        }
391        self.condition(" HAVING ", &s.having);
392        if !s.order_by.is_empty() {
393            self.push(" ORDER BY ");
394            self.list(&s.order_by, |w, (e, o)| {
395                w.expr(e);
396                w.push(match o {
397                    Order::Asc => " ASC",
398                    Order::Desc => " DESC",
399                });
400            });
401        }
402        // LIMIT and OFFSET are bound as parameters so that paging never
403        // changes the SQL text and the prepared statement stays cached. A
404        // value beyond i64::MAX is clamped rather than rejected.
405        if let Some(limit) = s.limit {
406            self.push(" LIMIT ");
407            self.param(Value::Integer(i64::try_from(limit).unwrap_or(i64::MAX)));
408        }
409        if let Some(offset) = s.offset {
410            // SQLite only accepts OFFSET after LIMIT; `LIMIT -1` means no
411            // limit.
412            if s.limit.is_none() {
413                self.push(" LIMIT -1");
414            }
415            self.push(" OFFSET ");
416            self.param(Value::Integer(i64::try_from(offset).unwrap_or(i64::MAX)));
417        }
418    }
419
420    /// Appends a `RETURNING` clause.
421    fn returning(&mut self, r: &Returning) {
422        match r {
423            Returning::None => {}
424            Returning::All => self.push(" RETURNING *"),
425            Returning::Columns(cols) => {
426                self.push(" RETURNING ");
427                self.list(cols, Self::column_ref);
428            }
429        }
430    }
431
432    /// Appends an `INSERT` statement.
433    fn insert(&mut self, s: &Insert) {
434        self.push("INSERT INTO ");
435        if let Some(t) = &s.table {
436            self.table_ref(t);
437        }
438        if s.default_values {
439            self.push(" DEFAULT VALUES");
440        } else {
441            if !s.columns.is_empty() {
442                self.push(" (");
443                self.list(&s.columns, Self::ident);
444                self.push(")");
445            }
446            if let Some(select) = &s.select {
447                self.push(" ");
448                self.select(select);
449            } else {
450                self.push(" VALUES ");
451                self.list(&s.rows, |w, row| {
452                    w.push("(");
453                    w.list(row, Self::expr);
454                    w.push(")");
455                });
456            }
457        }
458        if let Some(oc) = &s.on_conflict {
459            self.push(" ON CONFLICT");
460            if !oc.target.is_empty() {
461                self.push(" (");
462                self.list(&oc.target, Self::ident);
463                self.push(")");
464            }
465            match &oc.action {
466                ConflictAction::Nothing => self.push(" DO NOTHING"),
467                ConflictAction::Update(sets) => {
468                    self.push(" DO UPDATE SET ");
469                    self.list(sets, |w, (c, e)| {
470                        w.ident(c);
471                        w.push(" = ");
472                        w.expr(e);
473                    });
474                }
475            }
476        }
477        self.returning(&s.returning);
478    }
479
480    /// Appends an `UPDATE` statement.
481    fn update(&mut self, s: &Update) {
482        self.push("UPDATE ");
483        if let Some(t) = &s.table {
484            self.table_ref(t);
485        }
486        self.push(" SET ");
487        self.list(&s.sets, |w, (c, e)| {
488            w.ident(c);
489            w.push(" = ");
490            w.expr(e);
491        });
492        if let Some(cond) = &s.r#where {
493            self.condition(" WHERE ", cond);
494        }
495        self.returning(&s.returning);
496        if let Some(limit) = s.limit {
497            let _ = write!(self.sql, " LIMIT {limit}");
498        }
499    }
500
501    /// Appends a `DELETE` statement.
502    fn delete(&mut self, s: &Delete) {
503        self.push("DELETE FROM ");
504        if let Some(t) = &s.table {
505            self.table_ref(t);
506        }
507        if let Some(cond) = &s.r#where {
508            self.condition(" WHERE ", cond);
509        }
510        self.returning(&s.returning);
511    }
512
513    /// Appends a column definition.
514    fn column_def(&mut self, c: &ColumnDef) {
515        self.ident(&c.name);
516        self.push(" ");
517        self.push(c.ty.declared());
518        if c.primary_key {
519            self.push(" PRIMARY KEY");
520            if c.auto_increment {
521                self.push(" AUTOINCREMENT");
522            }
523        }
524        if c.not_null {
525            self.push(" NOT NULL");
526        }
527        if c.unique {
528            self.push(" UNIQUE");
529        }
530        if let Some(d) = &c.default {
531            self.push(" DEFAULT ");
532            self.default_expr(d);
533        }
534        if let Some(chk) = &c.check {
535            self.push(" CHECK (");
536            self.expr(chk);
537            self.push(")");
538        }
539    }
540
541    /// Appends a `DEFAULT` expression with its values inlined as literals.
542    ///
543    /// DDL cannot carry bound parameters, so this is the one place where
544    /// values are written into the SQL text. Anything that is not a plain
545    /// value is parenthesised, as SQLite requires for default expressions.
546    fn default_expr(&mut self, e: &Expr) {
547        match e {
548            Expr::Value(v) => self.push(&v.to_literal()),
549            Expr::Raw(sql, _) => {
550                self.push("(");
551                self.push(sql);
552                self.push(")");
553            }
554            other => {
555                let mut inner = Writer::default();
556                inner.expr(other);
557                let inlined = Statement {
558                    sql: inner.sql,
559                    values: inner.values,
560                }
561                .to_string_inlined();
562                self.push("(");
563                self.push(&inlined);
564                self.push(")");
565            }
566        }
567    }
568
569    /// Appends a foreign-key constraint.
570    fn foreign_key(&mut self, fk: &crate::schema::ForeignKey) {
571        if let Some(name) = &fk.name {
572            self.push("CONSTRAINT ");
573            self.ident(name);
574            self.push(" ");
575        }
576        self.push("FOREIGN KEY (");
577        self.list(&fk.columns, Self::ident);
578        self.push(") REFERENCES ");
579        self.ident(&fk.ref_table);
580        self.push(" (");
581        self.list(&fk.ref_columns, Self::ident);
582        self.push(")");
583        if let Some(a) = fk.on_delete {
584            self.push(" ON DELETE ");
585            self.push(a.sql());
586        }
587        if let Some(a) = fk.on_update {
588            self.push(" ON UPDATE ");
589            self.push(a.sql());
590        }
591    }
592
593    /// Appends a `CREATE TABLE` statement.
594    fn create_table(&mut self, s: &CreateTable) {
595        self.push("CREATE TABLE ");
596        if s.if_not_exists {
597            self.push("IF NOT EXISTS ");
598        }
599        if let Some(name) = &s.name {
600            self.ident(name);
601        }
602        self.push(" (");
603        self.list(&s.columns, Self::column_def);
604        for c in &s.constraints {
605            self.push(", ");
606            match c {
607                TableConstraint::PrimaryKey(cols) => {
608                    self.push("PRIMARY KEY (");
609                    self.list(cols, Self::ident);
610                    self.push(")");
611                }
612                TableConstraint::Unique(cols) => {
613                    self.push("UNIQUE (");
614                    self.list(cols, Self::ident);
615                    self.push(")");
616                }
617                TableConstraint::Check(e) => {
618                    self.push("CHECK (");
619                    self.expr(e);
620                    self.push(")");
621                }
622                TableConstraint::ForeignKey(fk) => self.foreign_key(fk),
623            }
624        }
625        self.push(")");
626        let mut opts = Vec::new();
627        if s.strict {
628            opts.push("STRICT");
629        }
630        if s.without_rowid {
631            opts.push("WITHOUT ROWID");
632        }
633        if !opts.is_empty() {
634            self.push(" ");
635            self.push(&opts.join(", "));
636        }
637    }
638
639    /// Appends an `ALTER TABLE` statement.
640    fn alter_table(&mut self, s: &AlterTable) {
641        self.push("ALTER TABLE ");
642        if let Some(name) = &s.name {
643            self.ident(name);
644        }
645        match &s.op {
646            Some(AlterOp::AddColumn(c)) => {
647                self.push(" ADD COLUMN ");
648                self.column_def(c);
649            }
650            Some(AlterOp::DropColumn(c)) => {
651                self.push(" DROP COLUMN ");
652                self.ident(c);
653            }
654            Some(AlterOp::RenameColumn(from, to)) => {
655                self.push(" RENAME COLUMN ");
656                self.ident(from);
657                self.push(" TO ");
658                self.ident(to);
659            }
660            Some(AlterOp::RenameTo(to)) => {
661                self.push(" RENAME TO ");
662                self.ident(to);
663            }
664            Some(AlterOp::AlterColumn(name, def)) => {
665                self.push(" ALTER COLUMN ");
666                self.ident(name);
667                self.push(" TO ");
668                self.column_def(def);
669            }
670            None => {}
671        }
672    }
673
674    /// Appends a `DROP TABLE` statement.
675    fn drop_table(&mut self, s: &DropTable) {
676        self.push("DROP TABLE ");
677        if s.if_exists {
678            self.push("IF EXISTS ");
679        }
680        if let Some(name) = &s.name {
681            self.ident(name);
682        }
683    }
684
685    /// Appends a `CREATE INDEX` statement.
686    fn create_index(&mut self, s: &CreateIndex) {
687        self.push("CREATE ");
688        if s.unique {
689            self.push("UNIQUE ");
690        }
691        self.push("INDEX ");
692        if s.if_not_exists {
693            self.push("IF NOT EXISTS ");
694        }
695        if let Some(name) = &s.name {
696            self.ident(name);
697        }
698        self.push(" ON ");
699        if let Some(table) = &s.table {
700            self.ident(table);
701        }
702        if let Some(method) = s.using {
703            self.push(" USING ");
704            self.push(method);
705        }
706        self.push(" (");
707        self.list(&s.columns, |w, (c, o)| {
708            w.ident(c);
709            match o {
710                Some(Order::Asc) => w.push(" ASC"),
711                Some(Order::Desc) => w.push(" DESC"),
712                None => {}
713            }
714        });
715        self.push(")");
716        if let Some(e) = &s.r#where {
717            self.push(" WHERE ");
718            self.expr(e);
719        }
720    }
721
722    /// Appends a `DROP INDEX` statement.
723    fn drop_index(&mut self, s: &DropIndex) {
724        self.push("DROP INDEX ");
725        if s.if_exists {
726            self.push("IF EXISTS ");
727        }
728        if let Some(name) = &s.name {
729            self.ident(name);
730        }
731    }
732
733    /// Consumes the writer into the rendered statement.
734    fn finish(self) -> Statement {
735        Statement {
736            sql: self.sql,
737            values: self.values,
738        }
739    }
740}
741
742/// Implements [`Build`] for a statement type by delegating to the named
743/// `Writer` method.
744macro_rules! impl_build {
745    ($($ty:ty => $method:ident),* $(,)?) => {$(
746        impl Build for $ty {
747            fn to_statement(&self) -> Statement {
748                let mut w = Writer::default();
749                w.$method(self);
750                w.finish()
751            }
752        }
753    )*};
754}
755
756impl_build!(
757    Select => select,
758    Insert => insert,
759    Update => update,
760    Delete => delete,
761    CreateTable => create_table,
762    AlterTable => alter_table,
763    DropTable => drop_table,
764    CreateIndex => create_index,
765    DropIndex => drop_index,
766);
767
768impl Build for Expr {
769    fn to_statement(&self) -> Statement {
770        let mut w = Writer::default();
771        w.expr(self);
772        w.finish()
773    }
774}
775
776impl Build for Statement {
777    fn to_statement(&self) -> Statement {
778        self.clone()
779    }
780}
781
782#[cfg(test)]
783mod tests {
784    use crate::prelude::*;
785
786    /// A select with aliases, a join, mixed `AND` / `OR` conditions,
787    /// grouping, ordering and paging renders in clause order with every
788    /// value bound.
789    #[test]
790    fn select_with_joins_and_conditions() {
791        let (sql, values) = Query::select()
792            .column(("u", "id"))
793            .expr_as(Func::count(Expr::col(("p", "id"))), "posts")
794            .from(TableRef::new("user").alias("u"))
795            .left_join(
796                TableRef::new("post").alias("p"),
797                Expr::col(("p", "user_id")).eq(Expr::col(("u", "id"))),
798            )
799            .and_where(
800                Condition::any()
801                    .add(Expr::col(("u", "name")).like("a%"))
802                    .add(Expr::col(("u", "id")).is_in([1, 2, 3])),
803            )
804            .and_where(Expr::col(("u", "deleted_at")).is_null())
805            .group_by(Expr::col(("u", "id")))
806            .and_having(Func::count(Expr::col(("p", "id"))).gt(0))
807            .order_by(("u", "id"), Order::Desc)
808            .limit(5)
809            .offset(10)
810            .build();
811        assert_eq!(
812            sql,
813            "SELECT \"u\".\"id\", COUNT(\"p\".\"id\") AS \"posts\" FROM \"user\" AS \"u\" \
814             LEFT JOIN \"post\" AS \"p\" ON \"p\".\"user_id\" = \"u\".\"id\" \
815             WHERE ((\"u\".\"name\" LIKE ?) OR \"u\".\"id\" IN (?, ?, ?)) AND \"u\".\"deleted_at\" IS NULL \
816             GROUP BY \"u\".\"id\" HAVING COUNT(\"p\".\"id\") > ? ORDER BY \"u\".\"id\" DESC LIMIT ? OFFSET ?"
817        );
818        assert_eq!(values.len(), 7);
819    }
820
821    /// Multi-row inserts with upsert, arithmetic updates and `NOT IN`
822    /// deletes render correctly, checked through the inlined form.
823    #[test]
824    fn insert_update_delete() {
825        let insert = Query::insert()
826            .into_table("user")
827            .columns(["name", "age"])
828            .values([Expr::val("bob"), Expr::val(30)])
829            .values([Expr::val("eve"), Expr::val(Option::<i32>::None)])
830            .on_conflict(OnConflict::update_columns(["name"], ["age"]))
831            .returning_all();
832        assert_eq!(
833            insert.to_string_inlined(),
834            "INSERT INTO \"user\" (\"name\", \"age\") VALUES ('bob', 30), ('eve', NULL) \
835             ON CONFLICT (\"name\") DO UPDATE SET \"age\" = \"excluded\".\"age\" RETURNING *"
836        );
837
838        let update = Query::update()
839            .table("user")
840            .value("age", Expr::col("age").add(1))
841            .and_where(Expr::col("id").eq(7));
842        assert_eq!(
843            update.to_string_inlined(),
844            "UPDATE \"user\" SET \"age\" = \"age\" + 1 WHERE \"id\" = 7"
845        );
846
847        let delete = Query::delete()
848            .from_table("user")
849            .and_where(Expr::col("id").is_not_in([1, 2]));
850        assert_eq!(
851            delete.to_string_inlined(),
852            "DELETE FROM \"user\" WHERE \"id\" NOT IN (1, 2)"
853        );
854    }
855
856    /// DDL renders column constraints in SQLite's order, inlines defaults
857    /// as literals and appends table options after the column list.
858    #[test]
859    fn ddl() {
860        let create = Table::create()
861            .table("post")
862            .if_not_exists()
863            .col(ColumnDef::integer("id").primary_key().auto_increment())
864            .col(ColumnDef::text("title").not_null().unique_key())
865            .col(ColumnDef::boolean("published").not_null().default(false))
866            .col(ColumnDef::integer("user_id").not_null())
867            .col(ColumnDef::date_time("created_at").not_null())
868            .foreign_key(
869                ForeignKey::new(["user_id"], "user", ["id"]).on_delete(ForeignKeyAction::Cascade),
870            )
871            .strict();
872        assert_eq!(
873            create.to_string_inlined(),
874            "CREATE TABLE IF NOT EXISTS \"post\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT, \
875             \"title\" TEXT NOT NULL UNIQUE, \"published\" INTEGER NOT NULL DEFAULT 0, \
876             \"user_id\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL, \
877             FOREIGN KEY (\"user_id\") REFERENCES \"user\" (\"id\") ON DELETE CASCADE) STRICT"
878        );
879        let index = CreateIndex::new()
880            .name("idx_post_user")
881            .table("post")
882            .col("user_id")
883            .if_not_exists();
884        assert_eq!(
885            index.to_string_inlined(),
886            "CREATE INDEX IF NOT EXISTS \"idx_post_user\" ON \"post\" (\"user_id\")"
887        );
888        let alter = Table::alter()
889            .table("post")
890            .add_column(ColumnDef::text("slug"));
891        assert_eq!(
892            alter.to_string_inlined(),
893            "ALTER TABLE \"post\" ADD COLUMN \"slug\" TEXT"
894        );
895        assert_eq!(
896            Table::drop().table("post").if_exists().to_string_inlined(),
897            "DROP TABLE IF EXISTS \"post\""
898        );
899    }
900
901    /// An `IN` over an empty list renders as `IN (NULL)`, which is valid
902    /// SQL that matches no row.
903    #[test]
904    fn empty_in_list_never_matches() {
905        let sql = Query::select()
906            .from("t")
907            .and_where(Expr::col("id").is_in(Vec::<i32>::new()))
908            .to_string_inlined();
909        assert_eq!(sql, "SELECT * FROM \"t\" WHERE \"id\" IN (NULL)");
910    }
911
912    /// The `contains` family escapes wildcards in the fragment and declares
913    /// the escape character, so `%` and `_` in user input match literally.
914    #[test]
915    fn contains_escapes_wildcards() {
916        let stmt = Query::select()
917            .from("t")
918            .and_where(Expr::col("name").contains("50%_a\\b"))
919            .to_statement();
920        assert_eq!(
921            stmt.sql,
922            "SELECT * FROM \"t\" WHERE \"name\" LIKE ? ESCAPE ?"
923        );
924        assert_eq!(
925            stmt.values,
926            vec![
927                Value::Text("%50\\%\\_a\\\\b%".into()),
928                Value::Text("\\".into())
929            ]
930        );
931        let plain = Query::select()
932            .from("t")
933            .and_where(Expr::col("name").not_like("a%"))
934            .to_string_inlined();
935        assert_eq!(plain, "SELECT * FROM \"t\" WHERE \"name\" NOT LIKE 'a%'");
936    }
937}