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}