Skip to main content

keelson_core/
writer.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::dialect::Dialect;
6use crate::error::{Error, Result};
7use crate::value::{ToValue, Value};
8
9/// A fragment of SQL that can render itself.
10///
11/// Rendering is infallible: appending to a `String` cannot fail, and neither can
12/// anything else an expression does. The one genuine failure — asking a dialect
13/// for a named argument it has no syntax for — is recorded on the writer with
14/// [`SqlWriter::record_error`] and surfaced once, by [`build`]. bob checks an
15/// error return more than fifteen times inside a single `SELECT`; none of that
16/// bookkeeping exists here.
17///
18/// The `Debug + Send + Sync` bounds are deliberate. Clauses store erased
19/// expressions, so a query must stay printable while debugging and holdable
20/// across an `.await` in the async execution layer; the bounds have to sit here
21/// rather than at every use site.
22pub trait Expression: fmt::Debug + Send + Sync {
23    /// Append this fragment to `w`.
24    ///
25    /// Every bound argument must go through [`SqlWriter::push_arg`]; that is the
26    /// only thing that advances the placeholder counter, which is what makes
27    /// nesting re-index correctly for free.
28    fn write_sql(&self, w: &mut SqlWriter<'_>);
29}
30
31/// The erased expression that clauses store.
32///
33/// `Arc` rather than `Box` because query structs derive `Clone` — build-time mods
34/// are applied to a clone of the query so that building stays `&self`.
35pub type DynExpr = Arc<dyn Expression>;
36
37/// Erase an expression into a [`DynExpr`].
38pub fn dyn_expr(e: impl Expression + 'static) -> DynExpr {
39    Arc::new(e)
40}
41
42/// A raw string is rendered verbatim.
43///
44/// This is bob's "progressive enhancement": anywhere an expression is accepted, a
45/// hand-written fragment works too. Covers `&str` of any lifetime through the
46/// blanket `&T` impl below, which includes the `&'static str` that clauses store.
47impl Expression for str {
48    fn write_sql(&self, w: &mut SqlWriter<'_>) {
49        w.push_str(self);
50    }
51}
52
53impl Expression for String {
54    fn write_sql(&self, w: &mut SqlWriter<'_>) {
55        w.push_str(self);
56    }
57}
58
59/// The form identifiers and raw SQL are stored in: a literal costs nothing and a
60/// computed string is owned, with no lifetime parameter leaking into query types.
61impl Expression for Cow<'_, str> {
62    fn write_sql(&self, w: &mut SqlWriter<'_>) {
63        w.push_str(self);
64    }
65}
66
67/// Numbers render as SQL literals, not as bound arguments.
68///
69/// bob's `Express` has a default arm that writes any non-expression with
70/// `fmt.Sprint`, and that is what makes `select::limit(20)` come out as
71/// `LIMIT 20`. Where a bound argument is wanted instead, the call is
72/// [`SqlWriter::push_arg`] or a dialect's `arg(..)` expression.
73macro_rules! impl_expression_for_number {
74    ($($t:ty),+) => {
75        $(
76            impl Expression for $t {
77                fn write_sql(&self, w: &mut SqlWriter<'_>) {
78                    w.push_str(&self.to_string());
79                }
80            }
81        )+
82    };
83}
84
85impl_expression_for_number!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64);
86
87impl<T: Expression + ?Sized> Expression for &T {
88    fn write_sql(&self, w: &mut SqlWriter<'_>) {
89        (**self).write_sql(w);
90    }
91}
92
93impl<T: Expression + ?Sized> Expression for Box<T> {
94    fn write_sql(&self, w: &mut SqlWriter<'_>) {
95        (**self).write_sql(w);
96    }
97}
98
99/// Covers [`DynExpr`] — `Arc<dyn Expression>` — as well as `Arc<ConcreteExpr>`.
100impl<T: Expression + ?Sized> Expression for Arc<T> {
101    fn write_sql(&self, w: &mut SqlWriter<'_>) {
102        (**self).write_sql(w);
103    }
104}
105
106/// An expression from a closure, for fragments with no natural struct — notably
107/// generated code.
108pub struct ExprFn<F>(F);
109
110/// Wrap a closure as an [`Expression`].
111pub fn expr_fn<F>(f: F) -> ExprFn<F>
112where
113    F: Fn(&mut SqlWriter<'_>) + Send + Sync,
114{
115    ExprFn(f)
116}
117
118impl<F> fmt::Debug for ExprFn<F> {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        f.write_str("ExprFn")
121    }
122}
123
124impl<F> Expression for ExprFn<F>
125where
126    F: Fn(&mut SqlWriter<'_>) + Send + Sync,
127{
128    fn write_sql(&self, w: &mut SqlWriter<'_>) {
129        (self.0)(w);
130    }
131}
132
133/// The SQL buffer, the bound arguments, the placeholder counter and the dialect,
134/// together.
135///
136/// bob passes `start int` down the tree and every caller adds `len(args)` by hand
137/// before recursing — `SelectQuery.WriteSQL` does it more than fifteen times.
138/// Here the counter lives next to the arguments and only
139/// [`push_arg`](Self::push_arg) touches it, so sub-queries and nested expressions
140/// re-index correctly with no bookkeeping at the call site.
141#[derive(Debug)]
142pub struct SqlWriter<'d> {
143    sql: String,
144    args: Vec<Value>,
145    dialect: &'d dyn Dialect,
146    next_arg: usize,
147    /// The first recorded failure. Kept rather than returned so that
148    /// [`Expression::write_sql`] can be infallible; [`finish`](Self::finish)
149    /// surfaces it.
150    error: Option<Error>,
151}
152
153impl<'d> SqlWriter<'d> {
154    /// A writer numbering placeholders from 1.
155    pub fn new(dialect: &'d dyn Dialect) -> Self {
156        Self::with_start(dialect, 1)
157    }
158
159    /// A writer numbering placeholders from `start`.
160    ///
161    /// Used to splice a query into one that already has arguments — bob's
162    /// `BuildN`.
163    ///
164    /// # Panics
165    /// If `start` is 0. Placeholders are 1-based in every supported dialect.
166    pub fn with_start(dialect: &'d dyn Dialect, start: usize) -> Self {
167        assert!(start > 0, "placeholder positions are 1-based, got {start}");
168        SqlWriter {
169            sql: String::new(),
170            args: Vec::new(),
171            dialect,
172            next_arg: start,
173            error: None,
174        }
175    }
176
177    /// The dialect this writer renders for.
178    pub fn dialect(&self) -> &'d dyn Dialect {
179        self.dialect
180    }
181
182    /// The SQL written so far.
183    pub fn sql(&self) -> &str {
184        &self.sql
185    }
186
187    /// The arguments bound so far, in placeholder order.
188    pub fn args(&self) -> &[Value] {
189        &self.args
190    }
191
192    /// The position the next [`push_arg`](Self::push_arg) will use (1-based).
193    pub fn arg_position(&self) -> usize {
194        self.next_arg
195    }
196
197    /// The first recorded failure, if any.
198    pub fn error(&self) -> Option<&Error> {
199        self.error.as_ref()
200    }
201
202    /// Record a failure.
203    ///
204    /// The first one wins: it is the one with the most context, and a later
205    /// failure is usually a consequence of it. Rendering continues either way —
206    /// the partial SQL is still useful in a debug print, and
207    /// [`finish`](Self::finish) is what refuses to hand it over.
208    pub fn record_error(&mut self, e: Error) {
209        if self.error.is_none() {
210            self.error = Some(e);
211        }
212    }
213
214    /// Append raw SQL.
215    pub fn push_str(&mut self, s: &str) {
216        self.sql.push_str(s);
217    }
218
219    /// Bind `v` and write its placeholder.
220    ///
221    /// The single point where the placeholder counter advances.
222    pub fn push_arg(&mut self, v: impl ToValue) {
223        let (d, pos) = (self.dialect, self.next_arg);
224        d.write_arg(self, pos);
225        self.args.push(v.to_value());
226        self.next_arg += 1;
227    }
228
229    /// Write a named argument's placeholder.
230    ///
231    /// Named arguments exist to prepare a statement whose values are supplied at
232    /// bind time, so nothing is added to the argument list and the positional
233    /// counter does not move.
234    ///
235    /// Records [`Error::NoNamedArgs`] if the dialect has no named-argument
236    /// syntax.
237    pub fn push_named_arg(&mut self, name: &str) {
238        let d = self.dialect;
239        d.write_named_arg(self, name);
240    }
241
242    /// Write a dotted, quoted identifier: `["users", "id"]` becomes
243    /// `"users"."id"`.
244    ///
245    /// Empty parts are skipped, so a caller can pass an unset qualifier without
246    /// branching. Generic over `AsRef<str>` so that a clause can hand over its
247    /// stored `[Cow<'static, str>]` directly.
248    pub fn push_quoted<S: AsRef<str>>(&mut self, parts: &[S]) {
249        let d = self.dialect;
250        let mut written = 0;
251        for part in parts {
252            let part = part.as_ref();
253            if part.is_empty() {
254                continue;
255            }
256            if written > 0 {
257                self.sql.push('.');
258            }
259            d.write_quoted(self, part);
260            written += 1;
261        }
262    }
263
264    /// Render a nested expression. Replaces bob's `Express`.
265    pub fn write_expr<E: Expression + ?Sized>(&mut self, e: &E) {
266        e.write_sql(self);
267    }
268
269    /// Render `prefix`, the expression, then `suffix` — but only if `cond`.
270    /// Replaces bob's `ExpressIf`.
271    ///
272    /// When `cond` is false nothing at all is written, affixes included, and no
273    /// argument is consumed.
274    pub fn write_if<E: Expression + ?Sized>(
275        &mut self,
276        cond: bool,
277        prefix: &str,
278        e: &E,
279        suffix: &str,
280    ) {
281        if !cond {
282            return;
283        }
284        self.push_str(prefix);
285        self.write_expr(e);
286        self.push_str(suffix);
287    }
288
289    /// [`write_if`](Self::write_if) for an optional clause, which is how most
290    /// clauses are stored.
291    pub fn write_if_some<E: Expression + ?Sized>(
292        &mut self,
293        e: Option<&E>,
294        prefix: &str,
295        suffix: &str,
296    ) {
297        if let Some(e) = e {
298            self.push_str(prefix);
299            self.write_expr(e);
300            self.push_str(suffix);
301        }
302    }
303
304    /// Render a slice joined by `sep` and wrapped in `prefix`/`suffix`, writing
305    /// nothing at all when the slice is empty. Replaces bob's `ExpressSlice`.
306    ///
307    /// The empty case is the load-bearing part: it is how a clause omits itself,
308    /// keyword and all.
309    pub fn write_slice<E: Expression>(
310        &mut self,
311        items: &[E],
312        prefix: &str,
313        sep: &str,
314        suffix: &str,
315    ) {
316        if items.is_empty() {
317            return;
318        }
319        self.push_str(prefix);
320        for (i, item) in items.iter().enumerate() {
321            if i > 0 {
322                self.push_str(sep);
323            }
324            self.write_expr(item);
325        }
326        self.push_str(suffix);
327    }
328
329    /// [`write_slice`](Self::write_slice) for anything iterable, so a clause can
330    /// map over its own storage without collecting into a `Vec` first.
331    pub fn write_iter<E, I>(&mut self, items: I, prefix: &str, sep: &str, suffix: &str)
332    where
333        E: Expression,
334        I: IntoIterator<Item = E>,
335    {
336        let mut it = items.into_iter().peekable();
337        if it.peek().is_none() {
338            return;
339        }
340        self.push_str(prefix);
341        for (i, item) in it.enumerate() {
342            if i > 0 {
343                self.push_str(sep);
344            }
345            self.write_expr(&item);
346        }
347        self.push_str(suffix);
348    }
349
350    /// Render a nested expression under a different dialect, keeping one shared
351    /// argument list, placeholder counter and error slot.
352    ///
353    /// This is how a sub-query built for one dialect embeds in a query built for
354    /// another — bob's `BaseQuery.WriteSQL` ignores the dialect handed to it and
355    /// uses its own.
356    pub fn write_with_dialect<E: Expression + ?Sized>(&mut self, dialect: &dyn Dialect, e: &E) {
357        // The borrowed dialect outlives only this call, so the nested writer gets
358        // a shorter lifetime and the buffers are moved through it and back.
359        let mut nested = SqlWriter {
360            sql: std::mem::take(&mut self.sql),
361            args: std::mem::take(&mut self.args),
362            dialect,
363            next_arg: self.next_arg,
364            error: self.error.take(),
365        };
366        e.write_sql(&mut nested);
367        self.sql = nested.sql;
368        self.args = nested.args;
369        self.next_arg = nested.next_arg;
370        self.error = nested.error;
371    }
372
373    /// Consume the writer, yielding the SQL and its arguments — or the recorded
374    /// failure.
375    pub fn finish(self) -> Result<(String, Vec<Value>)> {
376        match self.error {
377            Some(e) => Err(e),
378            None => Ok((self.sql, self.args)),
379        }
380    }
381
382    /// Consume the writer, yielding everything including any recorded failure.
383    ///
384    /// For a debug print that wants the partial SQL as well as the reason.
385    pub fn into_parts(self) -> (String, Vec<Value>, Option<Error>) {
386        (self.sql, self.args, self.error)
387    }
388}
389
390/// `write!` into the SQL buffer, for the rare fragment that is easier formatted
391/// than pushed.
392impl fmt::Write for SqlWriter<'_> {
393    fn write_str(&mut self, s: &str) -> fmt::Result {
394        self.sql.push_str(s);
395        Ok(())
396    }
397}
398
399/// Render an expression to SQL and arguments, numbering placeholders from 1.
400///
401/// This is what a query's `build()` calls, and the only place a recorded failure
402/// becomes a `Result`.
403pub fn build<E: Expression + ?Sized>(dialect: &dyn Dialect, e: &E) -> Result<(String, Vec<Value>)> {
404    build_from(dialect, 1, e)
405}
406
407/// [`build`] with a different first placeholder position — bob's `BuildN`.
408///
409/// # Panics
410/// If `start` is 0.
411pub fn build_from<E: Expression + ?Sized>(
412    dialect: &dyn Dialect,
413    start: usize,
414    e: &E,
415) -> Result<(String, Vec<Value>)> {
416    let mut w = SqlWriter::with_start(dialect, start);
417    e.write_sql(&mut w);
418    w.finish()
419}
420
421#[cfg(test)]
422mod tests {
423    use std::fmt::Write as _;
424
425    use keelson_sqlcheck::testing::assert_frag_sql;
426
427    use super::*;
428    use crate::dialect::testing::{Numbered, Positional, TestDialect};
429
430    /// Where a fragment of each shape is legal, for the cases that can be judged
431    /// as part of a statement. The rest of this module is about the writer's own
432    /// mechanics — a bracketed tree, a half-written buffer, an offset placeholder
433    /// run — and those are not SQL in any position; each says so where it stands.
434    const COND: &str = r#"SELECT "id" FROM users WHERE {}"#;
435    const VALUE: &str = r#"SELECT {} FROM users"#;
436
437    /// `"col" = $n`
438    #[derive(Debug)]
439    struct Eq(&'static str, i32);
440
441    impl Expression for Eq {
442        fn write_sql(&self, w: &mut SqlWriter<'_>) {
443            w.push_quoted(&[self.0]);
444            w.push_str(" = ");
445            w.push_arg(self.1);
446        }
447    }
448
449    /// A sub-select, to prove nesting re-indexes.
450    #[derive(Debug)]
451    struct Sub(Vec<Eq>);
452
453    impl Expression for Sub {
454        fn write_sql(&self, w: &mut SqlWriter<'_>) {
455            w.push_str("(SELECT 1 FROM users WHERE ");
456            w.write_slice(&self.0, "", " AND ", "");
457            w.push_str(")");
458        }
459    }
460
461    #[test]
462    fn placeholders_are_numbered_in_write_order() {
463        let (sql, args) = build(
464            &Numbered,
465            &Sub(vec![Eq("age", 10), Eq("id", 20), Eq("name", 30)]),
466        )
467        .unwrap();
468        assert_frag_sql(
469            r#"SELECT "id" FROM users WHERE "id" IN {}"#,
470            &sql,
471            r#"(SELECT 1 FROM users WHERE "age" = $1 AND "id" = $2 AND "name" = $3)"#,
472        );
473        assert_eq!(args, vec![Value::I32(10), Value::I32(20), Value::I32(30)]);
474    }
475
476    #[test]
477    fn nesting_continues_the_outer_numbering() {
478        #[derive(Debug)]
479        struct Outer;
480
481        impl Expression for Outer {
482            fn write_sql(&self, w: &mut SqlWriter<'_>) {
483                w.write_expr(&Eq("age", 1));
484                // `EXISTS`, because a sub-select in the middle of a conjunction has
485                // to be a condition rather than the single value it returns.
486                w.push_str(" AND EXISTS ");
487                w.write_expr(&Sub(vec![Eq("id", 2), Eq("name", 3)]));
488                w.push_str(" AND ");
489                w.write_expr(&Eq("email", 4));
490            }
491        }
492
493        let (sql, args) = build(&Numbered, &Outer).unwrap();
494        assert_frag_sql(
495            COND,
496            &sql,
497            concat!(
498                r#""age" = $1 AND EXISTS (SELECT 1 FROM users WHERE "id" = $2 AND "name" = $3)"#,
499                r#" AND "email" = $4"#
500            ),
501        );
502        assert_eq!(args.len(), 4);
503        assert_eq!(args[3], Value::I32(4));
504    }
505
506    /// Not judged: `($1 IN ($2 IN ($3)))` is a placeholder soup with nothing for
507    /// PostgreSQL to infer a type from, and the nesting is the point rather than
508    /// the SQL.
509    #[test]
510    fn a_subquery_three_levels_deep_never_restarts_numbering() {
511        // The bug this guards against is a nested expression building its own
512        // writer and starting from 1 again. Only push_arg moves the counter, and
513        // there is one counter, so it cannot happen by construction.
514        #[derive(Debug)]
515        struct Nest(usize);
516
517        impl Expression for Nest {
518            fn write_sql(&self, w: &mut SqlWriter<'_>) {
519                w.push_str("(");
520                w.push_arg(self.0 as i32);
521                if self.0 > 1 {
522                    w.push_str(" IN ");
523                    w.write_expr(&Nest(self.0 - 1));
524                }
525                w.push_str(")");
526            }
527        }
528
529        let (sql, args) = build(&Numbered, &Nest(3)).unwrap();
530        assert_eq!(sql, "($1 IN ($2 IN ($3)))");
531        assert_eq!(args, vec![Value::I32(3), Value::I32(2), Value::I32(1)]);
532    }
533
534    /// Not judged: the brackets are the test's own notation for tree shape, not
535    /// SQL syntax.
536    #[test]
537    fn interleaved_siblings_and_children_stay_in_write_order() {
538        #[derive(Debug)]
539        struct Pair(Box<dyn Expression>, Box<dyn Expression>);
540
541        impl Expression for Pair {
542            fn write_sql(&self, w: &mut SqlWriter<'_>) {
543                w.push_str("[");
544                w.write_expr(&self.0);
545                w.push_str(" ");
546                w.write_expr(&self.1);
547                w.push_str("]");
548            }
549        }
550
551        let tree = Pair(
552            Box::new(Pair(Box::new(Eq("a", 1)), Box::new(Eq("b", 2)))),
553            Box::new(Pair(Box::new(Eq("c", 3)), Box::new(Eq("d", 4)))),
554        );
555        let (sql, args) = build(&Numbered, &tree).unwrap();
556        assert_eq!(sql, r#"[["a" = $1 "b" = $2] ["c" = $3 "d" = $4]]"#);
557        assert_eq!(
558            args,
559            vec![Value::I32(1), Value::I32(2), Value::I32(3), Value::I32(4)]
560        );
561    }
562
563    /// Not judged: a fragment whose lowest placeholder is `$3` has no `$1`, and no
564    /// server will prepare that. Which is what `build_from` is for — splicing into
565    /// a statement that already has two arguments.
566    #[test]
567    fn build_from_offsets_the_first_placeholder() {
568        let (sql, args) = build_from(&Numbered, 3, &Sub(vec![Eq("age", 1), Eq("id", 2)])).unwrap();
569        assert_eq!(
570            sql,
571            r#"(SELECT 1 FROM users WHERE "age" = $3 AND "id" = $4)"#
572        );
573        assert_eq!(args.len(), 2, "args are still returned from the start");
574    }
575
576    #[test]
577    #[should_panic(expected = "1-based")]
578    fn start_zero_is_rejected() {
579        let _ = build_from(&Numbered, 0, &Eq("a", 1));
580    }
581
582    /// Not judged: `?` and backticks are MySQL's, and the judge reachable from
583    /// this crate is PostgreSQL's. `keelson-mysql` is where that dialect answers.
584    #[test]
585    fn positional_dialects_ignore_the_index_but_still_order_args() {
586        let (sql, args) = build(&Positional, &Sub(vec![Eq("age", 7), Eq("id", 8)])).unwrap();
587        assert_eq!(sql, "(SELECT 1 FROM users WHERE `age` = ? AND `id` = ?)");
588        assert_eq!(args, vec![Value::I32(7), Value::I32(8)]);
589    }
590
591    #[test]
592    fn arg_position_tracks_the_next_placeholder() {
593        let mut w = SqlWriter::new(&Numbered);
594        assert_eq!(w.arg_position(), 1);
595        w.push_arg(1i32);
596        assert_eq!(w.arg_position(), 2);
597        w.push_str(" -- not an arg");
598        assert_eq!(w.arg_position(), 2);
599        w.push_arg("two");
600        assert_eq!(w.arg_position(), 3);
601    }
602
603    #[test]
604    fn raw_strings_of_every_stored_form_are_expressions() {
605        let (sql, args) = build(&Numbered, "id = 1").unwrap();
606        assert_frag_sql(COND, &sql, "id = 1");
607        assert!(args.is_empty());
608
609        let (sql, _) = build(&Numbered, &String::from("id = 2")).unwrap();
610        assert_frag_sql(COND, &sql, "id = 2");
611
612        let borrowed: Cow<'static, str> = Cow::Borrowed("id = 3");
613        let (sql, _) = build(&Numbered, &borrowed).unwrap();
614        assert_frag_sql(COND, &sql, "id = 3");
615
616        let owned: Cow<'static, str> = Cow::Owned(String::from("id = 4"));
617        let (sql, _) = build(&Numbered, &owned).unwrap();
618        assert_frag_sql(COND, &sql, "id = 4");
619
620        let boxed: Box<dyn Expression> = Box::new(Eq("age", 1));
621        let (sql, _) = build(&Numbered, &boxed).unwrap();
622        assert_frag_sql(COND, &sql, r#""age" = $1"#);
623
624        let shared: DynExpr = dyn_expr(Eq("id", 2));
625        let (sql, _) = build(&Numbered, &shared).unwrap();
626        assert_frag_sql(COND, &sql, r#""id" = $1"#);
627    }
628
629    #[test]
630    fn numbers_render_as_literals_not_placeholders() {
631        let (sql, args) = build(&Numbered, &20i64).unwrap();
632        assert_frag_sql(VALUE, &sql, "20");
633        assert!(args.is_empty(), "a literal binds nothing");
634    }
635
636    #[test]
637    fn expr_fn_wraps_a_closure() {
638        let e = expr_fn(|w: &mut SqlWriter<'_>| {
639            w.push_str("LIMIT ");
640            w.push_arg(5i64);
641        });
642        let (sql, args) = build(&Numbered, &e).unwrap();
643        assert_frag_sql(r#"SELECT "id" FROM users {}"#, &sql, "LIMIT $1");
644        assert_eq!(args, vec![Value::I64(5)]);
645    }
646
647    #[test]
648    fn write_if_skips_everything_including_the_affixes() {
649        let mut w = SqlWriter::new(&Numbered);
650        w.write_if(false, " WHERE ", &Eq("a", 1), "!");
651        assert_eq!(w.sql(), "");
652        assert_eq!(w.arg_position(), 1, "a skipped arg must not advance");
653        w.write_if(true, " WHERE ", &Eq("a", 1), "!");
654        let (sql, args) = w.finish().unwrap();
655        assert_eq!(sql, r#" WHERE "a" = $1!"#);
656        assert_eq!(args.len(), 1);
657    }
658
659    #[test]
660    fn write_if_some_follows_the_option() {
661        let mut w = SqlWriter::new(&Numbered);
662        w.write_if_some(None::<&Eq>, " LIMIT ", "");
663        assert_eq!(w.sql(), "");
664        w.write_if_some(Some(&Eq("a", 1)), " WHERE ", ";");
665        assert_eq!(w.sql(), r#" WHERE "a" = $1;"#);
666    }
667
668    #[test]
669    fn write_slice_is_a_no_op_when_empty() {
670        let mut w = SqlWriter::new(&Numbered);
671        w.write_slice::<Eq>(&[], " WHERE ", " AND ", ";");
672        assert_eq!(w.sql(), "");
673
674        w.write_iter(Vec::<String>::new(), "(", ", ", ")");
675        assert_eq!(w.sql(), "");
676
677        w.write_iter(vec!["a", "b"], "(", ", ", ")");
678        assert_eq!(w.sql(), "(a, b)");
679    }
680
681    #[test]
682    fn push_quoted_joins_with_dots_and_drops_empty_parts() {
683        let mut w = SqlWriter::new(&Numbered);
684        w.push_quoted(&["users", "id"]);
685        w.push_str(" ");
686        w.push_quoted(&["", "id"]);
687        w.push_str(" ");
688        w.push_quoted::<&str>(&[]);
689        w.push_str(" ");
690        // The form a clause actually stores.
691        w.push_quoted(&[Cow::Borrowed("a"), Cow::Owned("b".to_owned())]);
692        assert_eq!(w.sql(), r#""users"."id" "id"  "a"."b""#);
693    }
694
695    #[test]
696    fn named_args_do_not_consume_an_arg_slot() {
697        let mut w = SqlWriter::new(&TestDialect);
698        w.push_arg(1i32);
699        w.push_str(", ");
700        w.push_named_arg("name");
701        w.push_str(", ");
702        w.push_arg(2i32);
703        let (sql, args) = w.finish().unwrap();
704        assert_eq!(sql, "?1, :name, ?2");
705        assert_eq!(args, vec![Value::I32(1), Value::I32(2)]);
706    }
707
708    #[test]
709    fn a_nested_dialect_shares_the_arg_list_and_counter() {
710        #[derive(Debug)]
711        struct Mixed;
712
713        impl Expression for Mixed {
714            fn write_sql(&self, w: &mut SqlWriter<'_>) {
715                w.write_expr(&Eq("a", 1));
716                w.push_str(" AND ");
717                w.write_with_dialect(&Positional, &Eq("b", 2));
718                w.push_str(" AND ");
719                w.write_expr(&Eq("c", 3));
720            }
721        }
722
723        let (sql, args) = build(&Numbered, &Mixed).unwrap();
724        assert_eq!(sql, r#""a" = $1 AND `b` = ? AND "c" = $3"#);
725        assert_eq!(
726            args.len(),
727            3,
728            "the counter advanced through the nested part"
729        );
730    }
731
732    #[test]
733    fn a_recorded_error_is_surfaced_by_build_and_only_by_build() {
734        #[derive(Debug)]
735        struct Bad;
736
737        impl Expression for Bad {
738            fn write_sql(&self, w: &mut SqlWriter<'_>) {
739                w.push_str("x = ");
740                w.push_named_arg("nope");
741            }
742        }
743
744        // write_sql itself cannot fail, so the SQL is still there...
745        let mut w = SqlWriter::new(&Numbered);
746        w.write_expr(&Bad);
747        assert_eq!(w.sql(), "x = ");
748        assert!(matches!(w.error(), Some(Error::NoNamedArgs)));
749
750        // ...and build is what refuses to hand it over.
751        assert!(matches!(build(&Numbered, &Bad), Err(Error::NoNamedArgs)));
752
753        // A failure inside a helper still propagates out of the whole build.
754        let mut w = SqlWriter::new(&Numbered);
755        w.write_slice(&[Bad], "(", ", ", ")");
756        assert!(w.finish().is_err());
757    }
758
759    #[test]
760    fn the_first_recorded_error_wins() {
761        let mut w = SqlWriter::new(&Numbered);
762        w.record_error(Error::Incomplete("a table"));
763        w.record_error(Error::NoNamedArgs);
764        let (_, _, err) = w.into_parts();
765        assert!(matches!(err, Some(Error::Incomplete("a table"))));
766    }
767
768    #[test]
769    fn fmt_write_appends_to_the_same_buffer() {
770        let mut w = SqlWriter::new(&Numbered);
771        write!(w, "OFFSET {}", 4).unwrap();
772        assert_eq!(w.sql(), "OFFSET 4");
773    }
774
775    #[test]
776    fn the_writer_exposes_its_dialect_to_nested_expressions() {
777        #[derive(Debug)]
778        struct UsesDialect;
779
780        impl Expression for UsesDialect {
781            fn write_sql(&self, w: &mut SqlWriter<'_>) {
782                let d = w.dialect();
783                d.write_quoted(w, "col");
784            }
785        }
786
787        let (sql, _) = build(&Positional, &UsesDialect).unwrap();
788        assert_eq!(sql, "`col`");
789    }
790}