Skip to main content

keelson_core/expr/
convert.rs

1use std::borrow::Cow;
2
3use crate::value::Value;
4use crate::writer::DynExpr;
5
6use super::node::Expr;
7
8/// Anything that can stand where an expression is expected.
9///
10/// This is bob's progressive enhancement, made a trait: a `&'static str` is raw
11/// SQL, a number is a SQL literal, a [`Value`] is a bound argument, and an
12/// [`Expr`] is itself. Every builder slot in keelson takes
13/// `impl IntoExpr`, so the same call site accepts a hand-written fragment today
14/// and a structured expression tomorrow with no change in shape.
15///
16/// # Which conversion each type gets, and why
17///
18/// | from | to | rationale |
19/// |---|---|---|
20/// | [`Expr`] | itself | |
21/// | `&'static str`, `String`, `Cow<'static, str>` | [`Expr::Raw`] | bob writes a Go `string` verbatim |
22/// | `i8`..`i64`, `u8`..`u64`, `f32`, `f64`, `bool` | [`Expr::Raw`] | bob's fallback formats any other value into the SQL, which is what makes `LIMIT 20` a literal |
23/// | [`Value`] | [`Expr::Arg`] | a `Value` is data, and data is bound, never interpolated |
24/// | [`DynExpr`] | [`Expr::Custom`] | the escape hatch for a dialect's own shape |
25///
26/// The split on the last two rows is the whole safety story: text that the
27/// program wrote is SQL, and text that came from outside is a `Value`. There is
28/// deliberately no impl turning `&str` into a bound string — that would make the
29/// same call site mean two different things depending on the author's intent, and
30/// bob does not do it either.
31///
32/// # Why `&'static str` and not `&str`
33///
34/// An [`Expr`] stores `Cow<'static, str>`, so that no public type needs a
35/// lifetime parameter. A `&'static str` — which is what a literal is — borrows
36/// for free; a shorter-lived `&str` would have to be copied on every conversion,
37/// silently allocating for the overwhelmingly common literal case. Pass a
38/// `String` when the text is computed.
39pub trait IntoExpr {
40    /// Perform the conversion.
41    fn into_expr(self) -> Expr;
42}
43
44/// A list of expressions: a tuple, an array, a `Vec`, `()` for none, or a single
45/// expression standing for a one-element list.
46///
47/// Function arguments, `IN (..)` operands, `AND` chains and row constructors all
48/// take one of these, so `f("LEAD", ("created_date", 1, f("NOW", ())))` works
49/// with a heterogeneous tuple while `in_(ids)` works with a `Vec`.
50///
51/// A tuple is how a *heterogeneous* list is written — Rust has no variadics, and
52/// this is the same trick that makes a tuple of [`Mod`](crate::Mod)s a `Mod`.
53pub trait IntoExprList {
54    /// Perform the conversion.
55    fn into_expr_list(self) -> Vec<Expr>;
56}
57
58/// The parts of a qualified identifier: `"age"`, or `("users", "id")`.
59///
60/// One entry point covers both, so a caller never has to decide between a
61/// singular and a plural helper. Empty parts are dropped by
62/// [`Expr::ident`](Expr::ident), which lets an unset table qualifier be passed
63/// straight through.
64pub trait IntoIdent {
65    /// Perform the conversion.
66    fn into_ident_parts(self) -> Vec<Cow<'static, str>>;
67}
68
69impl IntoExpr for Expr {
70    fn into_expr(self) -> Expr {
71        self
72    }
73}
74
75impl IntoExprList for Expr {
76    fn into_expr_list(self) -> Vec<Expr> {
77        vec![self]
78    }
79}
80
81/// No expressions at all — `f("NOW", ())`.
82impl IntoExprList for () {
83    fn into_expr_list(self) -> Vec<Expr> {
84        Vec::new()
85    }
86}
87
88impl IntoExpr for Value {
89    fn into_expr(self) -> Expr {
90        Expr::Arg(self)
91    }
92}
93
94impl IntoExprList for Value {
95    fn into_expr_list(self) -> Vec<Expr> {
96        vec![Expr::Arg(self)]
97    }
98}
99
100impl IntoExpr for DynExpr {
101    fn into_expr(self) -> Expr {
102        Expr::Custom(self)
103    }
104}
105
106impl IntoExprList for DynExpr {
107    fn into_expr_list(self) -> Vec<Expr> {
108        vec![Expr::Custom(self)]
109    }
110}
111
112/// Text is raw SQL, and doubles as a one-element list.
113macro_rules! impl_from_text {
114    ($($t:ty),+ $(,)?) => {
115        $(
116            impl IntoExpr for $t {
117                fn into_expr(self) -> Expr {
118                    Expr::Raw(self.into())
119                }
120            }
121
122            impl IntoExprList for $t {
123                fn into_expr_list(self) -> Vec<Expr> {
124                    vec![Expr::Raw(self.into())]
125                }
126            }
127        )+
128    };
129}
130
131impl_from_text!(&'static str, String, Cow<'static, str>);
132
133/// Numbers and booleans render as SQL literals, not as bound arguments — bob's
134/// `fmt.Sprint` fallback, and the reason `limit(20)` is `LIMIT 20`. Wrap the
135/// value in [`Expr::arg`] to bind it instead.
136macro_rules! impl_from_scalar {
137    ($($t:ty),+ $(,)?) => {
138        $(
139            impl IntoExpr for $t {
140                fn into_expr(self) -> Expr {
141                    Expr::Raw(Cow::Owned(self.to_string()))
142                }
143            }
144
145            impl IntoExprList for $t {
146                fn into_expr_list(self) -> Vec<Expr> {
147                    vec![self.into_expr()]
148                }
149            }
150        )+
151    };
152}
153
154impl_from_scalar!(
155    bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64
156);
157
158impl<T: IntoExpr, const N: usize> IntoExprList for [T; N] {
159    fn into_expr_list(self) -> Vec<Expr> {
160        self.into_iter().map(IntoExpr::into_expr).collect()
161    }
162}
163
164impl<T: IntoExpr> IntoExprList for Vec<T> {
165    fn into_expr_list(self) -> Vec<Expr> {
166        self.into_iter().map(IntoExpr::into_expr).collect()
167    }
168}
169
170macro_rules! impl_expr_list_tuple {
171    ($($name:ident),+) => {
172        #[allow(non_snake_case)]
173        impl<$($name: IntoExpr),+> IntoExprList for ($($name,)+) {
174            fn into_expr_list(self) -> Vec<Expr> {
175                let ($($name,)+) = self;
176                vec![$($name.into_expr()),+]
177            }
178        }
179    };
180}
181
182impl_expr_list_tuple!(A);
183impl_expr_list_tuple!(A, B);
184impl_expr_list_tuple!(A, B, C);
185impl_expr_list_tuple!(A, B, C, D);
186impl_expr_list_tuple!(A, B, C, D, E);
187impl_expr_list_tuple!(A, B, C, D, E, F);
188impl_expr_list_tuple!(A, B, C, D, E, F, G);
189impl_expr_list_tuple!(A, B, C, D, E, F, G, H);
190impl_expr_list_tuple!(A, B, C, D, E, F, G, H, I);
191impl_expr_list_tuple!(A, B, C, D, E, F, G, H, I, J);
192impl_expr_list_tuple!(A, B, C, D, E, F, G, H, I, J, K);
193impl_expr_list_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
194impl_expr_list_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M);
195impl_expr_list_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
196impl_expr_list_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
197impl_expr_list_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
198
199macro_rules! impl_ident_from_text {
200    ($($t:ty),+ $(,)?) => {
201        $(
202            impl IntoIdent for $t {
203                fn into_ident_parts(self) -> Vec<Cow<'static, str>> {
204                    vec![self.into()]
205                }
206            }
207        )+
208    };
209}
210
211impl_ident_from_text!(&'static str, String, Cow<'static, str>);
212
213impl<T: Into<Cow<'static, str>>, const N: usize> IntoIdent for [T; N] {
214    fn into_ident_parts(self) -> Vec<Cow<'static, str>> {
215        self.into_iter().map(Into::into).collect()
216    }
217}
218
219impl<T: Into<Cow<'static, str>>> IntoIdent for Vec<T> {
220    fn into_ident_parts(self) -> Vec<Cow<'static, str>> {
221        self.into_iter().map(Into::into).collect()
222    }
223}
224
225macro_rules! impl_ident_tuple {
226    ($($name:ident),+) => {
227        #[allow(non_snake_case)]
228        impl<$($name: Into<Cow<'static, str>>),+> IntoIdent for ($($name,)+) {
229            fn into_ident_parts(self) -> Vec<Cow<'static, str>> {
230                let ($($name,)+) = self;
231                vec![$($name.into()),+]
232            }
233        }
234    };
235}
236
237// A schema, a table, a column and an attribute is as deep as SQL goes.
238impl_ident_tuple!(A);
239impl_ident_tuple!(A, B);
240impl_ident_tuple!(A, B, C);
241impl_ident_tuple!(A, B, C, D);
242
243#[cfg(test)]
244mod tests {
245    use keelson_sqlcheck::testing::assert_frag_sql;
246
247    use super::*;
248    use crate::dialect::testing::Numbered;
249    use crate::writer::build;
250
251    const COND: &str = r#"SELECT "id" FROM users WHERE {}"#;
252    const VALUE: &str = r#"SELECT {} FROM users"#;
253    const EQ: &str = r#"SELECT "id" FROM users WHERE "age" = {}"#;
254
255    fn rendered(e: Expr) -> (String, Vec<Value>) {
256        build(&Numbered, &e).expect("render")
257    }
258
259    #[test]
260    fn text_becomes_raw_sql() {
261        for e in [
262            "age = 1".into_expr(),
263            String::from("age = 1").into_expr(),
264            Cow::Borrowed("age = 1").into_expr(),
265        ] {
266            assert!(matches!(e, Expr::Raw(_)));
267            assert_frag_sql(COND, &rendered(e).0, "age = 1");
268        }
269    }
270
271    #[test]
272    fn numbers_become_literals_and_values_become_arguments() {
273        let (sql, args) = rendered(20i64.into_expr());
274        assert_frag_sql(VALUE, &sql, "20");
275        assert!(args.is_empty(), "a literal binds nothing");
276
277        let (sql, args) = rendered(Value::I64(20).into_expr());
278        assert_frag_sql(EQ, &sql, "$1");
279        assert_eq!(args, vec![Value::I64(20)]);
280    }
281
282    #[test]
283    fn booleans_and_floats_render_as_written() {
284        assert_frag_sql(VALUE, &rendered(true.into_expr()).0, "true");
285        assert_frag_sql(VALUE, &rendered(1.5f64.into_expr()).0, "1.5");
286    }
287
288    #[test]
289    fn a_list_can_be_a_tuple_an_array_a_vec_or_nothing() {
290        assert_eq!(().into_expr_list().len(), 0);
291        assert_eq!(("a", 1, Value::I32(2)).into_expr_list().len(), 3);
292        assert_eq!(["a", "b"].into_expr_list().len(), 2);
293        assert_eq!(vec![1i32, 2, 3].into_expr_list().len(), 3);
294        assert_eq!(Expr::raw("a").into_expr_list().len(), 1);
295        assert_eq!("a".into_expr_list().len(), 1);
296    }
297
298    #[test]
299    fn a_heterogeneous_tuple_keeps_each_conversion() {
300        let list = ("created_date", 1i32, Value::Text("x".into())).into_expr_list();
301        assert!(matches!(list[0], Expr::Raw(_)));
302        assert!(matches!(list[1], Expr::Raw(_)));
303        assert!(matches!(list[2], Expr::Arg(_)));
304    }
305
306    #[test]
307    fn an_identifier_takes_one_part_or_several() {
308        assert_frag_sql(VALUE, &rendered(Expr::ident("age")).0, r#""age""#);
309        assert_frag_sql(
310            VALUE,
311            &rendered(Expr::ident(("users", "id"))).0,
312            r#""users"."id""#,
313        );
314        // A three-part identifier is schema-qualified, which resolves against an
315        // unqualified FROM as long as the schema is the right one.
316        assert_frag_sql(
317            VALUE,
318            &rendered(Expr::ident(["public", "users", "id"])).0,
319            r#""public"."users"."id""#,
320        );
321        assert_frag_sql(
322            VALUE,
323            &rendered(Expr::ident(vec![String::from("users"), String::from("id")])).0,
324            r#""users"."id""#,
325        );
326        // An unset qualifier needs no branch at the call site.
327        assert_frag_sql(VALUE, &rendered(Expr::ident(("", "id"))).0, r#""id""#);
328    }
329
330    #[test]
331    fn an_erased_expression_arrives_as_custom() {
332        let e: DynExpr = crate::writer::dyn_expr("1 + 1");
333        assert!(matches!(e.clone().into_expr(), Expr::Custom(_)));
334        assert_frag_sql(VALUE, &rendered(e.into_expr()).0, "1 + 1");
335    }
336}