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}