Skip to main content

keelson_core/expr/
mod.rs

1//! Expressions: one enum, its rendering, and the operator chain.
2//!
3//! This is the layer bob spreads over its `expr/` package. The design differs in
4//! one big way and follows bob closely in another.
5//!
6//! **The big difference.** bob has a struct per shape behind a
7//! `bob.Expression` interface; keelson has a single [`Expr`] enum. Expressions
8//! stay inspectable — which is what Layer 4's query rewriting needs — there is no
9//! dynamic dispatch on the hot path, `Clone` is cheap, and all the rendering
10//! decisions sit in one `match` where they can be read against the grammar.
11//! [`Expr::Custom`] holds an erased [`Expression`](crate::Expression) so a dialect
12//! can add shapes core has never heard of.
13//!
14//! **The close following.** Where the parentheses go, which separator a clause
15//! uses, when a fragment omits itself entirely — that is where bob's real value
16//! is, and it is reproduced exactly. In particular
17//! [`Expr::is_atomic`]/[`Expr::grouped`] is bob's `expr.X`, the rule that decides
18//! whether an expression is wrapped in parentheses, and it is the single most
19//! output-visible piece of logic in this module.
20//!
21//! # Entry points
22//!
23//! The free functions here mirror bob's `Builder` methods, which means they apply
24//! the parenthesisation rule; the associated constructors on [`Expr`] build a node
25//! and nothing more. For the atomic shapes — [`raw`], [`quote`], [`literal`],
26//! [`arg`] — the two are the same thing, because the rule leaves those alone.
27//!
28//! ```
29//! use keelson_core::expr::{Chain, arg, quote};
30//!
31//! # #[derive(Debug)] struct Psql;
32//! # impl keelson_core::Dialect for Psql {
33//! #     fn write_arg(&self, w: &mut keelson_core::SqlWriter<'_>, position: usize) {
34//! #         w.push_str("$"); w.push_str(&position.to_string());
35//! #     }
36//! #     fn write_quoted(&self, w: &mut keelson_core::SqlWriter<'_>, s: &str) {
37//! #         w.push_str("\""); w.push_str(s); w.push_str("\"");
38//! #     }
39//! # }
40//! let e = quote("age").gte(arg(21i32));
41//! let (sql, args) = keelson_core::build(&Psql, &e)?;
42//! assert_eq!(sql, r#"("age" >= $1)"#);
43//! # Ok::<_, keelson_core::Error>(())
44//! ```
45
46mod case;
47mod chain;
48mod convert;
49mod func;
50mod node;
51mod raw;
52
53pub use case::CaseBuilder;
54pub use chain::Chain;
55pub use convert::{IntoExpr, IntoExprList, IntoIdent};
56pub use func::FuncExpr;
57pub use node::Expr;
58pub use raw::RawArg;
59
60use std::borrow::Cow;
61
62use crate::value::ToValue;
63
64/// Raw SQL, written verbatim. `?` is *not* rewritten — see [`template`].
65///
66/// The progressive-enhancement entry point: a hand-written fragment goes anywhere
67/// a structured expression does.
68pub fn raw(sql: impl Into<Cow<'static, str>>) -> Expr {
69    Expr::raw(sql)
70}
71
72/// Raw SQL with `?` placeholders, rewritten into the dialect's own syntax with
73/// `args` interleaved. Write `\?` for a literal question mark.
74///
75/// A `?` may be filled by a value or by a whole expression, so
76/// `IN (?)` can expand to `IN ($3, $4, $5)`. The counts must match; a mismatch is
77/// [`Error::RawArgCount`](crate::Error::RawArgCount).
78pub fn template(sql: impl Into<Cow<'static, str>>, args: impl IntoIterator<Item = RawArg>) -> Expr {
79    Expr::template(sql, args)
80}
81
82/// A single-quoted SQL string literal — bob's `S()`. `literal("A")` renders `'A'`.
83///
84/// Nothing is escaped. This is for SQL the program wrote; text from outside
85/// belongs in [`arg`].
86pub fn literal(s: impl Into<Cow<'static, str>>) -> Expr {
87    Expr::literal(s)
88}
89
90/// A quoted identifier: `quote("age")` gives `"age"`, `quote(("users", "id"))`
91/// gives `"users"."id"`.
92pub fn quote(parts: impl IntoIdent) -> Expr {
93    Expr::ident(parts)
94}
95
96/// One bound argument, rendered as the dialect's placeholder.
97pub fn arg(v: impl ToValue) -> Expr {
98    Expr::arg(v)
99}
100
101/// Several bound arguments, comma-separated and *not* parenthesised — for slots
102/// that bring their own parentheses, such as `VALUES (..)`.
103pub fn args<V: ToValue>(vals: impl IntoIterator<Item = V>) -> Expr {
104    Expr::args(vals)
105}
106
107/// Several bound arguments, parenthesised: `($1, $2, $3)` — bob's `ArgGroup`.
108pub fn arg_group<V: ToValue>(vals: impl IntoIterator<Item = V>) -> Expr {
109    Expr::group(Expr::args(vals))
110}
111
112/// A named argument placeholder, for preparing a statement whose values arrive at
113/// bind time. Fails on a dialect with no named-argument syntax.
114pub fn named(name: impl Into<Cow<'static, str>>) -> Expr {
115    Expr::named_arg(name)
116}
117
118/// `n` unbound placeholders, comma-separated — bob's `Placeholder(n)`.
119pub fn placeholders(n: usize) -> Expr {
120    Expr::placeholders(n)
121}
122
123/// A parenthesised, comma-separated list. One element gives plain parentheses.
124pub fn group(items: impl IntoExprList) -> Expr {
125    Expr::group(items)
126}
127
128/// A function call, optionally windowed: `f("count", "*")`,
129/// `f("row_number", ()).over(w)`.
130pub fn f(name: impl Into<Cow<'static, str>>, args: impl IntoExprList) -> FuncExpr {
131    FuncExpr::new(name, args)
132}
133
134/// A `CASE` expression. See [`CaseBuilder`].
135pub fn case() -> CaseBuilder {
136    CaseBuilder::new()
137}
138
139/// `CAST(expr AS type_name)`.
140///
141/// Unlike bob's builder method this does not add outer parentheses: `CAST(..)` is
142/// already self-delimiting, so they would be pure noise. Wrap it in [`group`] if
143/// you want them.
144pub fn cast(expr: impl IntoExpr, type_name: impl Into<Cow<'static, str>>) -> Expr {
145    Expr::cast(expr, type_name)
146}
147
148/// `NOT expr`.
149///
150/// The operand is parenthesised if it needs it, the result is not — matching bob,
151/// where `Not` is the one builder method that does not wrap its own output. It
152/// does not need to: `NOT` binds looser than anything it can contain, so
153/// `NOT ("a" = $1)` is already unambiguous, and an enclosing operator will
154/// parenthesise it if one comes along.
155pub fn not(e: impl IntoExpr) -> Expr {
156    Expr::prefix("NOT", e.into_expr().grouped())
157}
158
159/// `(a AND b AND c)`.
160pub fn and(items: impl IntoExprList) -> Expr {
161    Expr::join_with(" AND ", items).grouped()
162}
163
164/// `(a OR b OR c)`.
165pub fn or(items: impl IntoExprList) -> Expr {
166    Expr::join_with(" OR ", items).grouped()
167}
168
169#[cfg(test)]
170mod tests {
171    use keelson_sqlcheck::testing::assert_frag_sql;
172
173    use super::*;
174    use crate::dialect::testing::Numbered;
175    use crate::value::Value;
176    use crate::writer::build;
177
178    /// Where a fragment of each shape is legal, and therefore judgeable. The
179    /// placeholders sit next to a column not because a lone one would be rejected —
180    /// PostgreSQL resolves a parameter it has nothing else to go on to `text` — but
181    /// because that is the position each of these entry points is *for*.
182    const COND: &str = r#"SELECT "id" FROM users WHERE {}"#;
183    const VALUE: &str = r#"SELECT {} FROM users"#;
184    const IN_LIST: &str = r#"SELECT "id" FROM users WHERE "id" IN ({})"#;
185    const ROW: &str = r#"SELECT "id" FROM users WHERE ("id", "age") = {}"#;
186    const EQ: &str = r#"SELECT "id" FROM users WHERE "id" = {}"#;
187
188    fn sql(e: Expr) -> String {
189        build(&Numbered, &e).expect("render").0
190    }
191
192    #[test]
193    fn the_atomic_entry_points_render_as_themselves() {
194        assert_frag_sql(COND, &sql(raw("age = 1")), "age = 1");
195        assert_frag_sql(VALUE, &sql(literal("A")), "'A'");
196        assert_frag_sql(VALUE, &sql(quote(("users", "id"))), r#""users"."id""#);
197        assert_frag_sql(EQ, &sql(arg(1i32)), "$1");
198        assert_frag_sql(IN_LIST, &sql(args([1i32, 2])), "$1, $2");
199        assert_frag_sql(ROW, &sql(arg_group([1i32, 2])), "($1, $2)");
200        assert_frag_sql(IN_LIST, &sql(placeholders(2)), "$1, $2");
201        assert_frag_sql(ROW, &sql(group(("id", "age"))), "(id, age)");
202    }
203
204    #[test]
205    fn boolean_combinators_parenthesise_their_result() {
206        assert_frag_sql(
207            COND,
208            &sql(and(("age > 1", "age < 9"))),
209            "(age > 1 AND age < 9)",
210        );
211        assert_frag_sql(
212            COND,
213            &sql(or(("age > 1", "age < 9"))),
214            "(age > 1 OR age < 9)",
215        );
216    }
217
218    #[test]
219    fn not_parenthesises_its_operand_but_not_itself() {
220        assert_frag_sql(
221            COND,
222            &sql(not(Expr::binary("age", "=", arg(1i32)))),
223            "NOT (age = $1)",
224        );
225        // Already atomic: no parentheses are added at all.
226        assert_frag_sql(COND, &sql(not(quote("is_active"))), r#"NOT "is_active""#);
227        // A chain result is already grouped, so `NOT` does not double-wrap it —
228        // the property that makes bob's "already a chain value" arm unnecessary.
229        assert_frag_sql(
230            COND,
231            &sql(not(quote("age").eq(arg(1i32)))),
232            r#"NOT ("age" = $1)"#,
233        );
234    }
235
236    #[test]
237    fn cast_is_not_wrapped_because_it_is_already_self_delimiting() {
238        assert_frag_sql(
239            VALUE,
240            &sql(cast(quote("age"), "int")),
241            r#"CAST("age" AS int)"#,
242        );
243    }
244
245    /// Not judged: `:id` is SQLite's spelling and PostgreSQL has no named
246    /// placeholders, so the judge reachable from here would reject it. What is
247    /// asserted is that it binds nothing.
248    #[test]
249    fn a_named_argument_binds_nothing() {
250        let (s, a) = build(&crate::dialect::testing::TestDialect, &named("id")).unwrap();
251        assert_eq!(s, ":id");
252        assert!(a.is_empty());
253    }
254
255    /// Not judged: one of each entry point in a row is a deliberate soup, not a
256    /// statement — `$1 ($2, $3) f($4) g($5)` is not legal in any position. What it
257    /// pins is that one counter runs through all five kinds, which is the property
258    /// a whole statement inherits.
259    #[test]
260    fn every_entry_point_shares_one_argument_counter() {
261        let e = Expr::join((
262            arg(1i32),
263            arg_group([2i32, 3]),
264            template("f(?)", [RawArg::value(4i32)]),
265            f("g", (arg(5i32),)).into_expr(),
266        ));
267        let (s, a) = build(&Numbered, &e).unwrap();
268        assert_eq!(s, "$1 ($2, $3) f($4) g($5)");
269        assert_eq!(
270            a,
271            vec![
272                Value::I32(1),
273                Value::I32(2),
274                Value::I32(3),
275                Value::I32(4),
276                Value::I32(5)
277            ]
278        );
279    }
280}