Skip to main content

keelson_core/expr/
node.rs

1use std::borrow::Cow;
2
3use crate::value::{ToValue, Value};
4use crate::writer::{DynExpr, Expression, SqlWriter, dyn_expr};
5
6use super::convert::{IntoExpr, IntoExprList, IntoIdent};
7use super::raw::{RawArg, write_template};
8
9/// A SQL expression, as data.
10///
11/// Where bob threads `bob.Expression` — a Go interface — through every slot and
12/// gives each shape its own unexported struct, keelson has one algebraic type.
13/// The reasons, in the order they matter:
14///
15/// 1. **Expressions stay inspectable.** Layer 4 rewrites a parsed query into
16///    clause-reconstruction code, and a rewriter needs to *look at* what it has.
17///    A `Box<dyn Expression>` can only be rendered.
18/// 2. **No dynamic dispatch on the hot path**, and `Clone` is a memcpy plus a
19///    couple of refcount bumps.
20/// 3. **One `match`** renders everything, so the spacing and separator decisions
21///    that bob spreads over a dozen files sit in one screen where they can be
22///    compared against the grammar.
23///
24/// The escape hatch is [`Expr::Custom`], which holds an erased
25/// [`Expression`]. Dialect-specific shapes — PostgreSQL's `ROWS FROM`, MySQL's
26/// index hints — live in their own crate as an ordinary `Expression` and travel
27/// through core as `Custom`, so `keelson-core` never learns about them.
28///
29/// This enum is deliberately *not* `#[non_exhaustive]`: exhaustive matching is
30/// the point, and a downstream rewriter that stops compiling when a variant is
31/// added is being told something true.
32///
33/// # Strings
34///
35/// Identifiers, raw SQL and type names are `Cow<'static, str>`: a literal borrows
36/// and costs nothing, a computed name is owned, and no lifetime parameter escapes
37/// into any public type. Operators and separators are `&'static str` — they are
38/// always literals, in core and in a dialect crate alike.
39#[derive(Debug, Clone)]
40pub enum Expr {
41    /// SQL written out verbatim. `?` is *not* rewritten — use
42    /// [`Expr::Template`] for that.
43    ///
44    /// This is bob's "progressive enhancement" in the enum: a hand-written
45    /// fragment is a first-class expression, and keyword fragments like `AND` or
46    /// `IS NULL` are nothing more than this.
47    Raw(Cow<'static, str>),
48
49    /// A single-quoted SQL string literal — bob's `S()`. Renders `'abc'`.
50    ///
51    /// Nothing is escaped, exactly as in bob. This is for keywords, enum labels
52    /// and other SQL the program itself wrote; user input belongs in
53    /// [`Expr::Arg`], where it is bound rather than interpolated.
54    Literal(Cow<'static, str>),
55
56    /// A dot-joined quoted identifier: `["users", "id"]` renders `"users"."id"`.
57    ///
58    /// Empty parts are skipped, so an unset qualifier needs no branch at the call
59    /// site, and an entirely empty list renders nothing at all.
60    Ident(Vec<Cow<'static, str>>),
61
62    /// One bound argument, rendered as the dialect's placeholder.
63    Arg(Value),
64
65    /// Several bound arguments, comma-separated: `$1, $2, $3`.
66    ///
67    /// Not parenthesised — it is usually written into a slot that brings its own
68    /// parentheses, such as `VALUES (..)`. Wrap it in [`Expr::Group`] when the
69    /// parentheses are wanted; that is what [`super::arg_group`] does. An empty
70    /// list renders `NULL`, matching bob.
71    Args(Vec<Value>),
72
73    /// A named argument placeholder, for preparing a statement whose values
74    /// arrive at bind time.
75    ///
76    /// Binds nothing and consumes no positional slot. On a dialect with no named
77    /// arguments this records [`Error::NoNamedArgs`](crate::Error::NoNamedArgs)
78    /// on the writer, which [`build`](crate::build) then surfaces.
79    NamedArg(Cow<'static, str>),
80
81    /// Raw SQL whose `?` placeholders are rewritten to the dialect's own syntax,
82    /// with `args` interleaved. See [`RawArg`] and [`super::template`].
83    Template {
84        /// The SQL, using `?` for every hole and `\?` for a literal question
85        /// mark.
86        sql: Cow<'static, str>,
87        /// One replacement per `?`, in order.
88        args: Vec<RawArg>,
89    },
90
91    /// A parenthesised, comma-separated list: `(a, b, c)`.
92    ///
93    /// One element is how a plain parenthesised expression is written. Empty
94    /// renders `(NULL)`, matching bob — a row constructor with no columns is
95    /// still a value.
96    Group(Vec<Expr>),
97
98    /// An infix operator: `lhs op rhs`, one space either side.
99    Binary {
100        /// Left operand.
101        lhs: Box<Expr>,
102        /// The operator, written verbatim between the operands.
103        op: &'static str,
104        /// Right operand.
105        rhs: Box<Expr>,
106    },
107
108    /// A prefix operator: `op operand`. `NOT x`, `-x`.
109    Prefix {
110        /// The operator.
111        op: &'static str,
112        /// The operand.
113        operand: Box<Expr>,
114    },
115
116    /// A postfix operator: `operand op`. `x IS NULL`, `x DESC`.
117    Postfix {
118        /// The operand.
119        operand: Box<Expr>,
120        /// The operator.
121        op: &'static str,
122    },
123
124    /// A separator-joined sequence — bob's `Join`. Renders nothing when empty.
125    ///
126    /// This is the general-purpose "several fragments in a row" node: `AND`/`OR`
127    /// chains, `BETWEEN a AND b`, a clause built out of keyword fragments.
128    Join {
129        /// The parts, in order.
130        exprs: Vec<Expr>,
131        /// Written between consecutive parts, verbatim. Use `" "` for bob's
132        /// default; see [`Expr::join`].
133        sep: &'static str,
134    },
135
136    /// A function call, optionally with an `OVER` window: `avg(x) OVER (w)`.
137    ///
138    /// Core keeps only what every dialect has. `DISTINCT`, `FILTER (WHERE ..)`
139    /// and `WITHIN GROUP` are per-dialect and belong to a dialect's own function
140    /// builder, reaching core through [`Expr::Custom`].
141    Func {
142        /// The function name, written verbatim — not quoted.
143        name: Cow<'static, str>,
144        /// The arguments, comma-separated.
145        args: Vec<Expr>,
146        /// The window definition or window name, rendered inside `OVER (..)`.
147        /// An empty expression is meaningful: `OVER ()`.
148        over: Option<Box<Expr>>,
149    },
150
151    /// `CASE WHEN c THEN t .. [ELSE e] END`.
152    ///
153    /// At least one `WHEN` is required; with none this records
154    /// [`Error::Incomplete`](crate::Error::Incomplete) and writes nothing, which
155    /// is bob's error turned into the recorded-failure form.
156    Case {
157        /// The `WHEN condition THEN result` pairs, in order.
158        whens: Vec<(Expr, Expr)>,
159        /// The `ELSE` branch.
160        else_: Option<Box<Expr>>,
161    },
162
163    /// `CAST(expr AS type_name)`.
164    Cast {
165        /// The expression being cast.
166        expr: Box<Expr>,
167        /// The target type, written verbatim — `int`, `numeric(10, 2)`.
168        type_name: Cow<'static, str>,
169    },
170
171    /// A dialect-specific expression core knows nothing about.
172    ///
173    /// The one place dynamic dispatch survives, and the reason core never needs a
174    /// variant for `ROWS FROM`, `MATCH .. AGAINST` or anything else that belongs
175    /// to exactly one grammar.
176    Custom(DynExpr),
177}
178
179impl Expr {
180    /// Raw SQL, verbatim. `?` is left alone.
181    pub fn raw(sql: impl Into<Cow<'static, str>>) -> Expr {
182        Expr::Raw(sql.into())
183    }
184
185    /// Raw SQL with `?` placeholders and their replacements.
186    pub fn template(
187        sql: impl Into<Cow<'static, str>>,
188        args: impl IntoIterator<Item = RawArg>,
189    ) -> Expr {
190        Expr::Template {
191            sql: sql.into(),
192            args: args.into_iter().collect(),
193        }
194    }
195
196    /// A single-quoted string literal — bob's `S()`.
197    pub fn literal(s: impl Into<Cow<'static, str>>) -> Expr {
198        Expr::Literal(s.into())
199    }
200
201    /// A quoted identifier. `ident("age")` and `ident(("users", "id"))` both
202    /// work; see [`IntoIdent`].
203    ///
204    /// Empty parts are dropped here rather than at render time, so the stored
205    /// node is exactly what will be written.
206    pub fn ident(parts: impl IntoIdent) -> Expr {
207        let mut parts = parts.into_ident_parts();
208        parts.retain(|p| !p.is_empty());
209        Expr::Ident(parts)
210    }
211
212    /// One bound argument.
213    pub fn arg(v: impl ToValue) -> Expr {
214        Expr::Arg(v.to_value())
215    }
216
217    /// A comma-separated list of bound arguments.
218    pub fn args<V: ToValue>(vals: impl IntoIterator<Item = V>) -> Expr {
219        Expr::Args(vals.into_iter().map(ToValue::to_value).collect())
220    }
221
222    /// `n` unbound placeholders — bob's `Placeholder(n)`.
223    ///
224    /// Each one binds `NULL`, so the shape of the statement is right and the
225    /// values are supplied by whatever rebinds it.
226    pub fn placeholders(n: usize) -> Expr {
227        Expr::Args(vec![Value::Null; n])
228    }
229
230    /// A named argument placeholder.
231    pub fn named_arg(name: impl Into<Cow<'static, str>>) -> Expr {
232        Expr::NamedArg(name.into())
233    }
234
235    /// A parenthesised list. A single expression gives plain parentheses.
236    pub fn group(items: impl IntoExprList) -> Expr {
237        Expr::Group(items.into_expr_list())
238    }
239
240    /// An infix operator applied to two operands.
241    pub fn binary(lhs: impl IntoExpr, op: &'static str, rhs: impl IntoExpr) -> Expr {
242        Expr::Binary {
243            lhs: Box::new(lhs.into_expr()),
244            op,
245            rhs: Box::new(rhs.into_expr()),
246        }
247    }
248
249    /// A prefix operator.
250    pub fn prefix(op: &'static str, operand: impl IntoExpr) -> Expr {
251        Expr::Prefix {
252            op,
253            operand: Box::new(operand.into_expr()),
254        }
255    }
256
257    /// A postfix operator.
258    pub fn postfix(operand: impl IntoExpr, op: &'static str) -> Expr {
259        Expr::Postfix {
260            operand: Box::new(operand.into_expr()),
261            op,
262        }
263    }
264
265    /// Space-separated parts — bob's `Join` with its default separator.
266    pub fn join(items: impl IntoExprList) -> Expr {
267        Expr::join_with(" ", items)
268    }
269
270    /// Parts joined by `sep`, written verbatim.
271    ///
272    /// Unlike bob, an empty separator means an empty separator. bob silently
273    /// substitutes a space for `Sep: ""`, which is a trap in a language where the
274    /// zero value is what you get by leaving a field out; here the separator is
275    /// always passed explicitly, and [`Expr::join`] is the space-separated form.
276    pub fn join_with(sep: &'static str, items: impl IntoExprList) -> Expr {
277        Expr::Join {
278            exprs: items.into_expr_list(),
279            sep,
280        }
281    }
282
283    /// A function call with no window.
284    pub fn func(name: impl Into<Cow<'static, str>>, args: impl IntoExprList) -> Expr {
285        Expr::Func {
286            name: name.into(),
287            args: args.into_expr_list(),
288            over: None,
289        }
290    }
291
292    /// `CAST(expr AS type_name)`.
293    pub fn cast(expr: impl IntoExpr, type_name: impl Into<Cow<'static, str>>) -> Expr {
294        Expr::Cast {
295            expr: Box::new(expr.into_expr()),
296            type_name: type_name.into(),
297        }
298    }
299
300    /// Wrap an arbitrary [`Expression`] so it can travel through core.
301    pub fn custom(e: impl Expression + 'static) -> Expr {
302        Expr::Custom(dyn_expr(e))
303    }
304
305    /// Whether this expression renders as a self-delimiting fragment, so that
306    /// parentheses around it would add nothing.
307    ///
308    /// # The parenthesisation rule
309    ///
310    /// This predicate and [`grouped`](Self::grouped) are bob's `expr.X` — the
311    /// single most output-visible decision in the whole library. Every operator
312    /// in bob's chain wraps its result in parentheses *unless* the result is one
313    /// of a small set of shapes, and that is precisely why bob emits
314    /// `("id" = $1)` for an equality but `"users"."id"` for a column.
315    ///
316    /// Atomic, and so never wrapped:
317    ///
318    /// - [`Raw`](Expr::Raw) and [`Template`](Expr::Template) — the author wrote
319    ///   the SQL and gets it back unedited.
320    /// - [`Literal`](Expr::Literal) — `'abc'` is one token.
321    /// - [`Ident`](Expr::Ident) — `"users"."id"` is one token.
322    /// - [`Arg`](Expr::Arg), [`Args`](Expr::Args), [`NamedArg`](Expr::NamedArg) —
323    ///   a placeholder list is normally written into a slot that supplies its own
324    ///   parentheses, such as `VALUES (..)`.
325    /// - [`Group`](Expr::Group) — already parenthesised.
326    ///
327    /// Everything else is wrapped, including [`Custom`](Expr::Custom): core
328    /// cannot see inside it, and bob's fallback for an unrecognised expression is
329    /// to wrap.
330    ///
331    /// bob has a further arm — an expression that is *already* a built chain
332    /// value is returned unchanged — which has no counterpart here and needs
333    /// none. Every chain step applies this rule to its own result, so a chain
334    /// value is always `Group` or atomic by construction, and re-applying the
335    /// rule is a no-op. That invariant is what makes `NOT ("a" = $1)` come out
336    /// with one set of parentheses rather than two.
337    pub fn is_atomic(&self) -> bool {
338        matches!(
339            self,
340            Expr::Raw(_)
341                | Expr::Template { .. }
342                | Expr::Literal(_)
343                | Expr::Ident(_)
344                | Expr::Arg(_)
345                | Expr::Args(_)
346                | Expr::NamedArg(_)
347                | Expr::Group(_)
348        )
349    }
350
351    /// Parenthesise this expression unless it [`is_atomic`](Self::is_atomic).
352    ///
353    /// This is bob's `expr.X`, and every operator in [`Chain`](super::Chain)
354    /// finishes with it.
355    #[must_use]
356    pub fn grouped(self) -> Expr {
357        if self.is_atomic() {
358            self
359        } else {
360            Expr::Group(vec![self])
361        }
362    }
363}
364
365impl Expression for Expr {
366    fn write_sql(&self, w: &mut SqlWriter<'_>) {
367        match self {
368            Expr::Raw(sql) => w.push_str(sql),
369
370            Expr::Literal(s) => {
371                w.push_str("'");
372                w.push_str(s);
373                w.push_str("'");
374            }
375
376            Expr::Ident(parts) => w.push_quoted(parts),
377
378            Expr::Arg(v) => w.push_arg(v.clone()),
379
380            // An empty list still has to render as a value; bob writes NULL and
381            // so must we, or `VALUES ()` comes out as a syntax error.
382            Expr::Args(vals) => {
383                if vals.is_empty() {
384                    w.push_str("NULL");
385                }
386                for (i, v) in vals.iter().enumerate() {
387                    if i > 0 {
388                        w.push_str(", ");
389                    }
390                    w.push_arg(v.clone());
391                }
392            }
393
394            Expr::NamedArg(name) => w.push_named_arg(name),
395
396            Expr::Template { sql, args } => write_template(w, sql, args),
397
398            Expr::Group(items) => {
399                if items.is_empty() {
400                    w.push_str("(NULL)");
401                } else {
402                    w.write_slice(items, "(", ", ", ")");
403                }
404            }
405
406            Expr::Binary { lhs, op, rhs } => {
407                w.write_expr(&**lhs);
408                w.push_str(" ");
409                w.push_str(op);
410                w.push_str(" ");
411                w.write_expr(&**rhs);
412            }
413
414            Expr::Prefix { op, operand } => {
415                w.push_str(op);
416                w.push_str(" ");
417                w.write_expr(&**operand);
418            }
419
420            Expr::Postfix { operand, op } => {
421                w.write_expr(&**operand);
422                w.push_str(" ");
423                w.push_str(op);
424            }
425
426            Expr::Join { exprs, sep } => w.write_slice(exprs, "", sep, ""),
427
428            Expr::Func { name, args, over } => {
429                w.push_str(name);
430                w.push_str("(");
431                w.write_slice(args, "", ", ", "");
432                w.push_str(")");
433                // bob writes `avg(x)OVER (w)` with no space, which is legal but
434                // reads like a typo. The space is free: the golden comparison
435                // normalises whitespace next to parentheses, so both forms clean
436                // to the same string.
437                w.write_if_some(over.as_deref(), " OVER (", ")");
438            }
439
440            Expr::Case { whens, else_ } => {
441                if whens.is_empty() {
442                    // bob returns an error here and writes nothing. Rendering is
443                    // infallible for us, so the failure is recorded and the
444                    // fragment omitted rather than half-written.
445                    w.record_error(crate::Error::Incomplete("a CASE WHEN branch"));
446                    return;
447                }
448                w.push_str("CASE");
449                for (cond, then) in whens {
450                    w.push_str(" WHEN ");
451                    w.write_expr(cond);
452                    w.push_str(" THEN ");
453                    w.write_expr(then);
454                }
455                w.write_if_some(else_.as_deref(), " ELSE ", "");
456                w.push_str(" END");
457            }
458
459            Expr::Cast { expr, type_name } => {
460                w.push_str("CAST(");
461                w.write_expr(&**expr);
462                w.push_str(" AS ");
463                w.push_str(type_name);
464                w.push_str(")");
465            }
466
467            Expr::Custom(e) => w.write_expr(&**e),
468        }
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use keelson_sqlcheck::testing::assert_frag_sql;
475
476    use super::*;
477    use crate::dialect::testing::{Numbered, TestDialect};
478    use crate::writer::build;
479
480    /// bob's dialect: `?N`, `:name`, `"quoted"`. Used where the case is about the
481    /// parenthesisation rule rather than about SQL a server would see.
482    fn sql(e: &Expr) -> String {
483        build(&TestDialect, e).expect("render").0
484    }
485
486    /// `$N` and `"quoted"` — what the psql judges understand.
487    fn pg(e: &Expr) -> String {
488        build(&Numbered, e).expect("render").0
489    }
490
491    /// Where a fragment of each shape is legal: a condition, a value, an `IN` list
492    /// and an `IN` list that brings its own parentheses.
493    const COND: &str = r#"SELECT "id" FROM users WHERE {}"#;
494    const VALUE: &str = r#"SELECT {} FROM users"#;
495    const IN_LIST: &str = r#"SELECT "id" FROM users WHERE "id" IN ({})"#;
496    const IN_GROUP: &str = r#"SELECT "id" FROM users WHERE "id" IN {}"#;
497
498    // --- the parenthesisation rule, arm by arm -------------------------------
499
500    #[test]
501    fn raw_sql_is_never_parenthesised() {
502        // The author wrote it; handing it back with parentheses added would be
503        // editing it.
504        let e = Expr::raw("age = 1");
505        assert!(e.is_atomic());
506        assert_frag_sql(COND, &pg(&e.clone().grouped()), "age = 1");
507        assert_frag_sql(COND, &pg(&e), "age = 1");
508    }
509
510    #[test]
511    fn a_template_is_never_parenthesised() {
512        let e = Expr::template("age = ?", [RawArg::value(1i32)]);
513        assert!(e.is_atomic());
514        assert_frag_sql(COND, &pg(&e.clone().grouped()), "age = $1");
515        // The same fragment under bob's dialect, where a hole is `?N`.
516        assert_eq!(sql(&e.grouped()), "age = ?1");
517    }
518
519    #[test]
520    fn a_string_literal_is_never_parenthesised() {
521        let e = Expr::literal("A");
522        assert!(e.is_atomic());
523        assert_frag_sql(VALUE, &pg(&e.grouped()), "'A'");
524    }
525
526    #[test]
527    fn a_quoted_identifier_is_never_parenthesised() {
528        let e = Expr::ident(("users", "id"));
529        assert!(e.is_atomic());
530        assert_frag_sql(VALUE, &pg(&e.grouped()), r#""users"."id""#);
531    }
532
533    #[test]
534    fn placeholders_are_never_parenthesised() {
535        // They land in slots that bring their own parentheses, such as VALUES.
536        for e in [Expr::arg(1i32), Expr::args([1i32, 2]), Expr::named_arg("n")] {
537            assert!(e.is_atomic(), "{e:?}");
538        }
539        assert_frag_sql(IN_LIST, &pg(&Expr::args([1i32, 2]).grouped()), "$1, $2");
540    }
541
542    #[test]
543    fn a_group_is_not_parenthesised_twice() {
544        let e = Expr::group(Expr::binary(Expr::ident("age"), "=", Expr::arg(1i32)));
545        assert!(e.is_atomic());
546        assert_frag_sql(COND, &pg(&e.grouped()), r#"("age" = $1)"#);
547    }
548
549    #[test]
550    fn every_operator_shape_is_parenthesised() {
551        let conditions: Vec<(Expr, &str)> = vec![
552            (
553                Expr::binary(Expr::ident("age"), "=", Expr::arg(1i32)),
554                r#"("age" = $1)"#,
555            ),
556            (
557                Expr::prefix("NOT", Expr::ident("is_active")),
558                r#"(NOT "is_active")"#,
559            ),
560            (
561                Expr::postfix(Expr::ident("age"), "IS NULL"),
562                r#"("age" IS NULL)"#,
563            ),
564        ];
565        for (e, expected) in conditions {
566            assert!(!e.is_atomic(), "{e:?} should not be atomic");
567            assert_frag_sql(COND, &pg(&e.grouped()), expected);
568        }
569
570        let values: Vec<(Expr, &str)> = vec![
571            (Expr::func("NOW", ()), "(NOW())"),
572            (
573                Expr::cast(Expr::ident("age"), "int"),
574                r#"(CAST("age" AS int))"#,
575            ),
576            (
577                Expr::Case {
578                    whens: vec![(Expr::raw("age > 1"), Expr::literal("x"))],
579                    else_: None,
580                },
581                "(CASE WHEN age > 1 THEN 'x' END)",
582            ),
583        ];
584        for (e, expected) in values {
585            assert!(!e.is_atomic(), "{e:?} should not be atomic");
586            assert_frag_sql(VALUE, &pg(&e.grouped()), expected);
587        }
588
589        // A `Join` is parenthesised by the same rule, and this one is the reason
590        // the rule cannot be judged shape by shape: `("age" DESC)` is legal in no
591        // statement position at all — a sort key's direction is part of the ORDER
592        // BY, not of an expression — so the assertion is the rendering.
593        let sort_key = Expr::join([Expr::ident("age"), Expr::raw("DESC")]);
594        assert!(!sort_key.is_atomic());
595        assert_eq!(pg(&sort_key.grouped()), r#"("age" DESC)"#);
596    }
597
598    #[test]
599    fn a_custom_expression_is_parenthesised_because_core_cannot_see_inside_it() {
600        #[derive(Debug)]
601        struct Opaque;
602        impl Expression for Opaque {
603            fn write_sql(&self, w: &mut SqlWriter<'_>) {
604                w.push_str("ARRAY[1] <@ ARRAY[1, 2]");
605            }
606        }
607        let e = Expr::custom(Opaque);
608        assert!(!e.is_atomic());
609        assert_frag_sql(COND, &pg(&e.grouped()), "(ARRAY[1] <@ ARRAY[1, 2])");
610    }
611
612    #[test]
613    fn grouping_is_idempotent_which_is_what_keeps_operators_from_nesting_parens() {
614        let once = Expr::binary(Expr::ident("age"), "=", Expr::arg(1i32)).grouped();
615        let twice = once.clone().grouped();
616        assert_frag_sql(COND, &pg(&once), r#"("age" = $1)"#);
617        assert_eq!(pg(&once), pg(&twice));
618    }
619
620    // --- rendering -----------------------------------------------------------
621
622    /// Not judged: an identifier that renders nothing leaves a hole in whatever
623    /// held it, and `SELECT  FROM users` is not a statement — which is why the
624    /// clauses check `is_empty` before writing a separator.
625    #[test]
626    fn an_empty_identifier_renders_nothing_and_empty_parts_are_dropped() {
627        assert_eq!(sql(&Expr::ident(Vec::<String>::new())), "");
628        assert_frag_sql(VALUE, &pg(&Expr::ident(["", "id"])), r#""id""#);
629        assert!(matches!(Expr::ident(["", "id"]), Expr::Ident(p) if p.len() == 1));
630    }
631
632    #[test]
633    fn an_empty_argument_list_renders_null() {
634        assert_frag_sql(IN_LIST, &pg(&Expr::args(Vec::<i32>::new())), "NULL");
635    }
636
637    #[test]
638    fn an_empty_group_renders_a_null_row() {
639        assert_frag_sql(IN_GROUP, &pg(&Expr::Group(vec![])), "(NULL)");
640    }
641
642    #[test]
643    fn placeholders_bind_null_and_keep_their_positions() {
644        let (s, args) = build(&Numbered, &Expr::placeholders(3)).unwrap();
645        assert_frag_sql(IN_LIST, &s, "$1, $2, $3");
646        assert!(args.iter().all(Value::is_null));
647    }
648
649    /// Not judged: `:name` is SQLite's spelling, and the dialect that has it is not
650    /// the one the judges here understand.
651    #[test]
652    fn a_named_argument_binds_nothing_and_fails_where_unsupported() {
653        let (s, args) = build(&TestDialect, &Expr::named_arg("name")).unwrap();
654        assert_eq!(s, ":name");
655        assert!(args.is_empty());
656        assert!(matches!(
657            build(&Numbered, &Expr::named_arg("name")),
658            Err(crate::Error::NoNamedArgs)
659        ));
660    }
661
662    #[test]
663    fn a_function_call_renders_its_arguments_and_window() {
664        assert_frag_sql(VALUE, &pg(&Expr::func("NOW", ())), "NOW()");
665        // `LEAD` is a window function, so the frame supplies the `OVER` its
666        // arguments would otherwise be checked without.
667        assert_frag_sql(
668            "SELECT {} OVER () FROM posts",
669            &pg(&Expr::func(
670                "LEAD",
671                ("published_at", 1, Expr::func("NOW", ())),
672            )),
673            "LEAD(published_at, 1, NOW())",
674        );
675        assert_frag_sql(
676            VALUE,
677            &pg(&Expr::Func {
678                name: "row_number".into(),
679                args: vec![],
680                over: Some(Box::new(Expr::raw(""))),
681            }),
682            "row_number() OVER ()",
683        );
684    }
685
686    #[test]
687    fn case_renders_both_with_and_without_an_else() {
688        let with_else = Expr::Case {
689            whens: vec![(
690                Expr::binary(Expr::ident("id"), "=", Expr::literal("1")).grouped(),
691                Expr::literal("A"),
692            )],
693            else_: Some(Box::new(Expr::literal("B"))),
694        };
695        assert_frag_sql(
696            VALUE,
697            &pg(&with_else),
698            r#"CASE WHEN ("id" = '1') THEN 'A' ELSE 'B' END"#,
699        );
700
701        let without = Expr::Case {
702            whens: vec![(Expr::raw("age > 1"), Expr::literal("A"))],
703            else_: None,
704        };
705        assert_frag_sql(VALUE, &pg(&without), "CASE WHEN age > 1 THEN 'A' END");
706    }
707
708    #[test]
709    fn a_case_with_no_branches_is_a_recorded_failure_not_a_broken_fragment() {
710        let empty = Expr::Case {
711            whens: vec![],
712            else_: None,
713        };
714        let err = build(&TestDialect, &empty).unwrap_err();
715        // The substring names the SQL concept (a CASE WHEN branch), not the
716        // message wording.
717        assert!(
718            matches!(&err, crate::Error::Incomplete(what) if what.contains("CASE WHEN")),
719            "got: {err}"
720        );
721    }
722
723    /// Not judged: two expressions with a separator between them is a *list*, and
724    /// `a b` or `ab` is not a fragment any statement position accepts. The
725    /// separator is the writer's contract, checked here as written.
726    #[test]
727    fn join_uses_its_separator_verbatim() {
728        let parts = [Expr::raw("a"), Expr::raw("b")];
729        assert_eq!(sql(&Expr::join(parts.clone())), "a b");
730        assert_eq!(sql(&Expr::join_with(" || ", parts.clone())), "a || b");
731        assert_eq!(sql(&Expr::join_with("", parts)), "ab");
732    }
733
734    /// Not judged, and could not be: what an empty join renders is the *absence*
735    /// of SQL, which is how a clause omits itself.
736    #[test]
737    fn an_empty_join_renders_nothing_which_is_how_a_clause_omits_itself() {
738        assert_eq!(sql(&Expr::join(Vec::<Expr>::new())), "");
739    }
740
741    /// Not judged: every operand is a placeholder, on both sides of the `IN`, so
742    /// PostgreSQL has nothing to infer a type from — and a row constructor against
743    /// a list mixing a row and a scalar is a semantic error besides. The typed
744    /// version of this shape is `chain::tests::a_row_constructor_in_a_list_of_row_constructors`;
745    /// what is left here is the write order of nested groups.
746    #[test]
747    fn nested_arguments_are_numbered_in_write_order() {
748        let e = Expr::binary(
749            Expr::group(Expr::args([1i32, 2])),
750            "IN",
751            Expr::group([Expr::group(Expr::args([3i32, 4])), Expr::arg(5i32)]),
752        );
753        let (s, args) = build(&Numbered, &e).unwrap();
754        assert_eq!(s, "($1, $2) IN (($3, $4), $5)");
755        assert_eq!(args.len(), 5);
756    }
757}