keelson_core/expr/chain.rs
1use super::convert::{IntoExpr, IntoExprList, IntoIdent};
2use super::node::Expr;
3
4/// The operator chain: `quote("age").gte(arg(21))`.
5///
6/// Every method builds a node around `self` and then applies the
7/// parenthesisation rule ([`Expr::grouped`]), which is what bob's `expr.X` does
8/// at the end of each of its chain methods. Because the rule is idempotent and
9/// treats [`Expr::Group`] as atomic, a chain value is always "already
10/// parenthesised or not needing it", and successive steps never pile up
11/// redundant parentheses.
12///
13/// # How a dialect adds an operator
14///
15/// bob parameterises `Chain[T, B]` over the dialect's own expression type so that
16/// `keelson-psql` can add `@>` without touching core. In Rust the same
17/// extensibility comes from a trait with default methods, and there are two ways
18/// to take it — neither needs a change to core.
19///
20/// **An extension trait**, which is the normal case and needs no new type:
21///
22/// ```
23/// use keelson_core::expr::{Chain, Expr, IntoExpr, arg, quote};
24///
25/// // PostgreSQL-only operators, reachable only where this trait is imported.
26/// trait PsqlOps: Chain {
27/// /// `@>` — contains. Nothing but a symbol, so `op` is the whole story.
28/// fn contains(self, rhs: impl IntoExpr) -> Self {
29/// self.op("@>", rhs)
30/// }
31///
32/// /// A shape `op` cannot express, built through `step` so that the
33/// /// parenthesisation rule is still applied for us.
34/// fn between_symmetric(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
35/// self.step(move |lhs| {
36/// Expr::join((lhs, Expr::raw("BETWEEN SYMMETRIC"), a, Expr::raw("AND"), b))
37/// })
38/// }
39/// }
40///
41/// impl<T: Chain> PsqlOps for T {}
42///
43/// # #[derive(Debug)] struct Psql;
44/// # impl keelson_core::Dialect for Psql {
45/// # fn write_arg(&self, w: &mut keelson_core::SqlWriter<'_>, position: usize) {
46/// # w.push_str("$"); w.push_str(&position.to_string());
47/// # }
48/// # fn write_quoted(&self, w: &mut keelson_core::SqlWriter<'_>, s: &str) {
49/// # w.push_str("\""); w.push_str(s); w.push_str("\"");
50/// # }
51/// # }
52/// let (sql, _) = keelson_core::build(&Psql, "e("tags").contains(arg("x")))?;
53/// assert_eq!(sql, r#"("tags" @> $1)"#);
54/// # Ok::<_, keelson_core::Error>(())
55/// ```
56///
57/// **A newtype**, when the dialect wants its operators to be unreachable from
58/// another dialect's expressions rather than merely un-imported:
59///
60/// ```
61/// use keelson_core::expr::{Chain, Expr, IntoExpr, IntoExprList};
62///
63/// #[derive(Debug, Clone)]
64/// struct PsqlExpr(Expr);
65///
66/// impl IntoExpr for PsqlExpr {
67/// fn into_expr(self) -> Expr { self.0 }
68/// }
69///
70/// impl IntoExprList for PsqlExpr {
71/// fn into_expr_list(self) -> Vec<Expr> { vec![self.0] }
72/// }
73///
74/// impl Chain for PsqlExpr {
75/// fn from_expr(e: Expr) -> Self { PsqlExpr(e) }
76/// }
77///
78/// impl PsqlExpr {
79/// fn contains(self, rhs: impl IntoExpr) -> Self { self.op("@>", rhs) }
80/// }
81///
82/// // Core operators return `PsqlExpr`, so a dialect operator still applies after
83/// // one: the chain never escapes into a type that has lost `@>`.
84/// let e = PsqlExpr::from_expr(Expr::ident("tags")).contains("'{a}'").is_not_null();
85/// ```
86///
87/// Either way [`step`](Self::step) is the only thing an added operator needs, and
88/// the parenthesisation rule is applied for it.
89///
90/// # Why the supertraits
91///
92/// A chain has to be able to hand its expression back — to nest it in another
93/// operator, or to store it in a clause. That is exactly what [`IntoExpr`] means,
94/// so `Chain` requires it instead of declaring a second method with the same job,
95/// and any chain value can therefore be passed to any slot in the library.
96/// [`IntoExprList`] is required for the same reason one step out: it is what lets
97/// `a.and(b)` take another chain value directly instead of the one-element tuple
98/// `(b,)`. Both are one line for a dialect newtype, and requiring them here is how
99/// that requirement gets stated rather than discovered.
100// `is_null`, `is_not_null` and `is_distinct_from` take `self` by value like every
101// other operator here. They are SQL keywords being spelled out, not Rust
102// predicates returning `bool`, and renaming them to satisfy the convention would
103// make the chain read less like the SQL it produces.
104#[allow(clippy::wrong_self_convention)]
105pub trait Chain: IntoExpr + IntoExprList + Sized {
106 /// Wrap a finished expression back into the chain type.
107 ///
108 /// The inverse of [`IntoExpr::into_expr`]; the two together are all a dialect
109 /// has to supply.
110 fn from_expr(e: Expr) -> Self;
111
112 /// One chain step: build a node from this expression, apply the
113 /// parenthesisation rule, and return a chain again.
114 ///
115 /// Every operator below is a one-line call to this, and so is every operator
116 /// a dialect adds.
117 #[must_use]
118 fn step(self, f: impl FnOnce(Expr) -> Expr) -> Self {
119 Self::from_expr(f(self.into_expr()).grouped())
120 }
121
122 /// An arbitrary infix operator: `self op rhs`.
123 ///
124 /// The generic escape hatch — a dialect-specific operator that needs nothing
125 /// but a symbol is this call and no new code in core.
126 #[must_use]
127 fn op(self, op: &'static str, rhs: impl IntoExpr) -> Self {
128 self.step(move |lhs| Expr::binary(lhs, op, rhs))
129 }
130
131 /// `self = rhs`.
132 #[must_use]
133 fn eq(self, rhs: impl IntoExpr) -> Self {
134 self.op("=", rhs)
135 }
136
137 /// `self <> rhs`. The standard spelling, which every dialect accepts.
138 #[must_use]
139 fn ne(self, rhs: impl IntoExpr) -> Self {
140 self.op("<>", rhs)
141 }
142
143 /// `self < rhs`.
144 #[must_use]
145 fn lt(self, rhs: impl IntoExpr) -> Self {
146 self.op("<", rhs)
147 }
148
149 /// `self <= rhs`.
150 #[must_use]
151 fn lte(self, rhs: impl IntoExpr) -> Self {
152 self.op("<=", rhs)
153 }
154
155 /// `self > rhs`.
156 #[must_use]
157 fn gt(self, rhs: impl IntoExpr) -> Self {
158 self.op(">", rhs)
159 }
160
161 /// `self >= rhs`.
162 #[must_use]
163 fn gte(self, rhs: impl IntoExpr) -> Self {
164 self.op(">=", rhs)
165 }
166
167 /// `self IN (a, b, c)`.
168 ///
169 /// The operands are always parenthesised, so a single sub-select operand
170 /// comes out as `IN (SELECT ..)` and a row list as `IN ((..), (..))`.
171 #[must_use]
172 fn in_(self, vals: impl IntoExprList) -> Self {
173 self.step(move |lhs| Expr::binary(lhs, "IN", Expr::group(vals)))
174 }
175
176 /// `self NOT IN (a, b, c)`.
177 #[must_use]
178 fn not_in(self, vals: impl IntoExprList) -> Self {
179 self.step(move |lhs| Expr::binary(lhs, "NOT IN", Expr::group(vals)))
180 }
181
182 /// `self IS NULL`.
183 #[must_use]
184 fn is_null(self) -> Self {
185 self.step(|lhs| Expr::postfix(lhs, "IS NULL"))
186 }
187
188 /// `self IS NOT NULL`.
189 #[must_use]
190 fn is_not_null(self) -> Self {
191 self.step(|lhs| Expr::postfix(lhs, "IS NOT NULL"))
192 }
193
194 /// `self IS DISTINCT FROM rhs`.
195 #[must_use]
196 fn is_distinct_from(self, rhs: impl IntoExpr) -> Self {
197 self.op("IS DISTINCT FROM", rhs)
198 }
199
200 /// `self IS NOT DISTINCT FROM rhs`.
201 #[must_use]
202 fn is_not_distinct_from(self, rhs: impl IntoExpr) -> Self {
203 self.op("IS NOT DISTINCT FROM", rhs)
204 }
205
206 /// `self BETWEEN a AND b`.
207 #[must_use]
208 fn between(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
209 self.step(move |lhs| Expr::join((lhs, Expr::raw("BETWEEN"), a, Expr::raw("AND"), b)))
210 }
211
212 /// `self NOT BETWEEN a AND b`.
213 #[must_use]
214 fn not_between(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
215 self.step(move |lhs| Expr::join((lhs, Expr::raw("NOT BETWEEN"), a, Expr::raw("AND"), b)))
216 }
217
218 /// `self LIKE rhs`.
219 #[must_use]
220 fn like(self, rhs: impl IntoExpr) -> Self {
221 self.op("LIKE", rhs)
222 }
223
224 /// `self || a || b` — string concatenation.
225 #[must_use]
226 fn concat(self, others: impl IntoExprList) -> Self {
227 self.step(move |lhs| Expr::join_with(" || ", prepend(lhs, others)))
228 }
229
230 /// `self AND a AND b`.
231 #[must_use]
232 fn and(self, others: impl IntoExprList) -> Self {
233 self.step(move |lhs| Expr::join_with(" AND ", prepend(lhs, others)))
234 }
235
236 /// `self OR a OR b`.
237 #[must_use]
238 fn or(self, others: impl IntoExprList) -> Self {
239 self.step(move |lhs| Expr::join_with(" OR ", prepend(lhs, others)))
240 }
241
242 /// `self + rhs`.
243 #[must_use]
244 fn plus(self, rhs: impl IntoExpr) -> Self {
245 self.op("+", rhs)
246 }
247
248 /// `self - rhs`.
249 #[must_use]
250 fn minus(self, rhs: impl IntoExpr) -> Self {
251 self.op("-", rhs)
252 }
253
254 /// `self AS "alias"`.
255 ///
256 /// This one ends the chain — it returns an [`Expr`], not `Self`. An alias is
257 /// not an operand: nothing may be applied to `x AS "y"`, and unlike every
258 /// other method here the result is deliberately *not* parenthesised, because
259 /// `(x AS "y")` is a syntax error in a select list.
260 fn as_(self, alias: impl IntoIdent) -> Expr {
261 Expr::Binary {
262 lhs: Box::new(self.into_expr()),
263 op: "AS",
264 rhs: Box::new(Expr::ident(alias)),
265 }
266 }
267}
268
269/// `self` first, then the rest — the shape every variadic chain method needs.
270fn prepend(first: Expr, rest: impl IntoExprList) -> Vec<Expr> {
271 let mut exprs = rest.into_expr_list();
272 exprs.insert(0, first);
273 exprs
274}
275
276/// An [`Expr`] is its own chain, so operators are available without any wrapper
277/// type. A dialect that wants its own type implements [`Chain`] for that instead.
278impl Chain for Expr {
279 fn from_expr(e: Expr) -> Expr {
280 e
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use keelson_sqlcheck::testing::assert_frag_sql;
287
288 use super::super::{arg, arg_group, literal, quote, raw};
289 use super::*;
290 use crate::dialect::testing::{Numbered, TestDialect};
291 use crate::value::Value;
292 use crate::writer::build;
293
294 /// An operator expression is a fragment. A boolean one is judged where a
295 /// condition goes and a scalar one where a value goes — putting either in the
296 /// other's place is a mistake the grammar or the engine will name.
297 const COND: &str = r#"SELECT "id" FROM users WHERE {}"#;
298 const VALUE: &str = r#"SELECT {} FROM users"#;
299 const POST_COND: &str = r#"SELECT "id" FROM posts WHERE {}"#;
300
301 fn sql(e: Expr) -> String {
302 build(&Numbered, &e).expect("render").0
303 }
304
305 #[test]
306 fn a_comparison_is_parenthesised_exactly_once() {
307 assert_frag_sql(COND, &sql(quote("age").gte(arg(21i32))), r#"("age" >= $1)"#);
308 }
309
310 #[test]
311 fn every_comparison_operator_uses_its_standard_spelling() {
312 // Chapter 9 of the PostgreSQL manual for each spelling; `<>` rather than
313 // `!=` because that is the standard one.
314 let conditions = [
315 (quote("age").eq(raw("id")), r#"("age" = id)"#),
316 (quote("age").ne(raw("id")), r#"("age" <> id)"#),
317 (quote("age").lt(raw("id")), r#"("age" < id)"#),
318 (quote("age").lte(raw("id")), r#"("age" <= id)"#),
319 (quote("age").gt(raw("id")), r#"("age" > id)"#),
320 (quote("age").gte(raw("id")), r#"("age" >= id)"#),
321 (quote("name").like(literal("b%")), r#"("name" LIKE 'b%')"#),
322 ];
323 for (e, expected) in conditions {
324 assert_frag_sql(COND, &sql(e), expected);
325 }
326
327 // Arithmetic is a value, not a condition, so it goes in the select list.
328 let values = [
329 (quote("age").plus(1i32), r#"("age" + 1)"#),
330 (quote("age").minus(quote("id")), r#"("age" - "id")"#),
331 ];
332 for (e, expected) in values {
333 assert_frag_sql(VALUE, &sql(e), expected);
334 }
335
336 // A dialect operator core has never heard of, reached through `op`.
337 assert_frag_sql(
338 COND,
339 &sql(raw("ARRAY[1, 2]").op("@>", raw("ARRAY[1]"))),
340 "(ARRAY[1, 2] @> ARRAY[1])",
341 );
342 }
343
344 #[test]
345 fn null_tests_are_postfix() {
346 assert_frag_sql(COND, &sql(quote("age").is_null()), r#"("age" IS NULL)"#);
347 assert_frag_sql(
348 COND,
349 &sql(quote("age").is_not_null()),
350 r#"("age" IS NOT NULL)"#,
351 );
352 }
353
354 #[test]
355 fn distinct_from_is_an_infix_keyword_operator() {
356 assert_frag_sql(
357 COND,
358 &sql(quote("age").is_distinct_from(quote("id"))),
359 r#"("age" IS DISTINCT FROM "id")"#,
360 );
361 assert_frag_sql(
362 COND,
363 &sql(quote("age").is_not_distinct_from(quote("id"))),
364 r#"("age" IS NOT DISTINCT FROM "id")"#,
365 );
366 }
367
368 #[test]
369 fn between_keeps_its_three_part_shape() {
370 assert_frag_sql(
371 COND,
372 &sql(quote("age").between(arg(1i32), arg(2i32))),
373 r#"("age" BETWEEN $1 AND $2)"#,
374 );
375 assert_frag_sql(
376 COND,
377 &sql(quote("age").not_between(arg(1i32), arg(2i32))),
378 r#"("age" NOT BETWEEN $1 AND $2)"#,
379 );
380 }
381
382 #[test]
383 fn in_always_parenthesises_its_operands() {
384 assert_frag_sql(
385 POST_COND,
386 &sql(quote("status").in_((literal("A"), literal("B")))),
387 r#"("status" IN ('A', 'B'))"#,
388 );
389 assert_frag_sql(
390 COND,
391 &sql(quote("id").not_in(arg(1i32))),
392 r#"("id" NOT IN ($1))"#,
393 );
394 }
395
396 /// The shape bob's `select with grouped IN` fixture pins: a row constructor
397 /// on the left, a list of row constructors on the right.
398 #[test]
399 fn a_row_constructor_in_a_list_of_row_constructors() {
400 let e = Expr::group((quote("id"), quote("user_id")))
401 .in_((arg_group([1i32, 2]), arg_group([3i32, 4])));
402 let (rendered, args) = build(&Numbered, &e).unwrap();
403 assert_frag_sql(
404 POST_COND,
405 &rendered,
406 r#"(("id", "user_id") IN (($1, $2), ($3, $4)))"#,
407 );
408 assert_eq!(args.len(), 4);
409 }
410
411 #[test]
412 fn boolean_chains_take_one_operand_or_several() {
413 assert_frag_sql(
414 COND,
415 &sql(quote("is_active").and(raw("age > 1"))),
416 r#"("is_active" AND age > 1)"#,
417 );
418 assert_frag_sql(
419 COND,
420 &sql(quote("is_active").or((raw("age > 1"), raw("age < 9")))),
421 r#"("is_active" OR age > 1 OR age < 9)"#,
422 );
423 }
424
425 /// The `psql upsert` fixture's `SET` value, which is where concatenation is
426 /// pinned. `EXCLUDED` only exists inside `ON CONFLICT DO UPDATE`, so that is
427 /// the frame.
428 #[test]
429 fn concat_joins_with_the_pipe_operator() {
430 let e = raw(r#"EXCLUDED."name""#).concat((
431 literal(" (formerly "),
432 quote(("tags", "name")),
433 literal(")"),
434 ));
435 assert_frag_sql(
436 r#"INSERT INTO tags ("id", "name") VALUES (1, 'rust') ON CONFLICT ("id") DO UPDATE SET "name" = {}"#,
437 &sql(e),
438 r#"(EXCLUDED."name" || ' (formerly ' || "tags"."name" || ')')"#,
439 );
440 }
441
442 #[test]
443 fn nesting_a_chain_in_a_chain_adds_no_extra_parentheses() {
444 // Each step's result is already atomic, so re-applying the rule is a
445 // no-op. This is the invariant that replaces bob's "already a chain
446 // value" arm.
447 let e = quote("age").eq(arg(1i32)).and(quote("id").eq(arg(2i32)));
448 assert_frag_sql(COND, &sql(e), r#"(("age" = $1) AND ("id" = $2))"#);
449 }
450
451 #[test]
452 fn an_alias_ends_the_chain_and_is_not_parenthesised() {
453 let e = quote("age").minus(quote("id")).as_("difference");
454 assert_frag_sql(VALUE, &sql(e), r#"("age" - "id") AS "difference""#);
455 }
456
457 #[test]
458 fn arguments_are_numbered_left_to_right_across_a_whole_chain() {
459 let e = quote("age")
460 .between(arg(1i32), arg(2i32))
461 .and(quote("id").in_((arg(3i32), arg(4i32))));
462 let (rendered, args) = build(&Numbered, &e).unwrap();
463 assert_frag_sql(
464 COND,
465 &rendered,
466 r#"(("age" BETWEEN $1 AND $2) AND ("id" IN ($3, $4)))"#,
467 );
468 assert_eq!(
469 args,
470 vec![Value::I32(1), Value::I32(2), Value::I32(3), Value::I32(4)]
471 );
472 }
473
474 /// The extension shape the three dialect crates depend on: a trait of default
475 /// methods over `Chain`, blanket-implemented, adding an operator core has
476 /// never heard of.
477 #[test]
478 fn a_dialect_can_add_an_operator_without_touching_core() {
479 trait PsqlOps: Chain {
480 fn contains(self, rhs: impl IntoExpr) -> Self {
481 self.op("@>", rhs)
482 }
483
484 fn between_symmetric(self, a: impl IntoExpr, b: impl IntoExpr) -> Self {
485 self.step(move |lhs| {
486 Expr::join((lhs, Expr::raw("BETWEEN SYMMETRIC"), a, Expr::raw("AND"), b))
487 })
488 }
489 }
490 impl<T: Chain> PsqlOps for T {}
491
492 assert_frag_sql(
493 COND,
494 &sql(raw("ARRAY[1, 2]").contains(raw("ARRAY[1]"))),
495 "(ARRAY[1, 2] @> ARRAY[1])",
496 );
497 assert_frag_sql(
498 COND,
499 &sql(quote("age").between_symmetric(arg(1i32), arg(2i32))),
500 r#"("age" BETWEEN SYMMETRIC $1 AND $2)"#,
501 );
502 }
503
504 /// The other extension shape: a dialect newtype, so its operators cannot be
505 /// reached from another dialect's expressions at all.
506 ///
507 /// Not judged: `GLOB` is SQLite's, and the dialect this renders under is
508 /// SQLite-shaped (`?N`, `:name`). PostgreSQL — the judge reachable from
509 /// `keelson-core` — has neither, and `keelson-sqlite` is where the operator
510 /// itself is checked. What is asserted here is that the chain stays in the
511 /// newtype.
512 #[test]
513 fn a_dialect_newtype_keeps_the_whole_chain_in_its_own_type() {
514 #[derive(Debug, Clone)]
515 struct SqliteExpr(Expr);
516
517 impl IntoExpr for SqliteExpr {
518 fn into_expr(self) -> Expr {
519 self.0
520 }
521 }
522
523 impl IntoExprList for SqliteExpr {
524 fn into_expr_list(self) -> Vec<Expr> {
525 vec![self.0]
526 }
527 }
528
529 impl Chain for SqliteExpr {
530 fn from_expr(e: Expr) -> Self {
531 SqliteExpr(e)
532 }
533 }
534
535 impl SqliteExpr {
536 fn glob(self, rhs: impl IntoExpr) -> Self {
537 self.op("GLOB", rhs)
538 }
539 }
540
541 // Chaining a core operator returns the dialect's type, so a
542 // dialect-specific one still applies afterwards.
543 let e = SqliteExpr::from_expr(Expr::ident("name"))
544 .glob(literal("a*"))
545 .and(SqliteExpr::from_expr(Expr::ident("b")).is_null());
546 let (s, _) = build(&TestDialect, &e.into_expr()).unwrap();
547 assert_eq!(s, r#"(("name" GLOB 'a*') AND ("b" IS NULL))"#);
548 }
549}