Skip to main content

keelson_core/expr/
raw.rs

1use std::borrow::Cow;
2
3use crate::error::Error;
4use crate::value::{ToValue, Value};
5use crate::writer::SqlWriter;
6
7use super::convert::IntoExpr;
8use super::node::Expr;
9
10/// One replacement for a `?` in an [`Expr::Template`].
11///
12/// A `?` either binds a value or splices in a whole expression. The second form
13/// is what lets `WHERE id IN (?)` expand to `IN ($3, $4, $5)`: the nested
14/// expression consumes as many placeholder positions as it binds arguments, and
15/// the counter keeps going from there.
16#[derive(Debug, Clone)]
17pub enum RawArg {
18    /// Bind a value; the `?` becomes the dialect's positional placeholder.
19    Value(Value),
20    /// Render an expression in place of the `?`.
21    Expr(Expr),
22    /// Write a named placeholder. Binds nothing, consumes no position, and
23    /// records [`Error::NoNamedArgs`] on a dialect that has no named-argument
24    /// syntax.
25    Named(Cow<'static, str>),
26}
27
28impl RawArg {
29    /// A bound value.
30    pub fn value(v: impl ToValue) -> RawArg {
31        RawArg::Value(v.to_value())
32    }
33
34    /// A spliced expression.
35    pub fn expr(e: impl IntoExpr) -> RawArg {
36        RawArg::Expr(e.into_expr())
37    }
38
39    /// A named placeholder.
40    pub fn named(name: impl Into<Cow<'static, str>>) -> RawArg {
41        RawArg::Named(name.into())
42    }
43}
44
45/// Write `sql` with every unescaped `?` replaced by the corresponding `args`
46/// entry, rewritten into the dialect's own placeholder syntax.
47///
48/// `\?` is a literal question mark. The rewrite is a single left-to-right byte
49/// scan — both `?` and `\` are ASCII, so byte offsets are always on character
50/// boundaries.
51///
52/// A count mismatch is recorded rather than short-circuited: the whole statement
53/// is still written first so that the error names the clause the author actually
54/// typed, and a missing argument binds `NULL` to keep the placeholder positions
55/// after it correct. That is bob's behaviour too, and it is why the recorded
56/// fixture for the mismatched case has full SQL *and* an error.
57pub(super) fn write_template(w: &mut SqlWriter<'_>, sql: &str, args: &[RawArg]) {
58    let mut rest = sql;
59    let mut placeholders = 0usize;
60
61    loop {
62        let Some(mark) = rest.find('?') else {
63            w.push_str(rest);
64            break;
65        };
66
67        // An escape only matters for the `?` we are looking at. A `\?` further
68        // along belongs to a later iteration, and a `\?` we have already passed
69        // is no longer in `rest`.
70        if let Some(escape) = rest.find("\\?").filter(|escape| mark > *escape) {
71            w.push_str(&rest[..escape]);
72            w.push_str("?");
73            rest = &rest[escape + 2..];
74            continue;
75        }
76
77        w.push_str(&rest[..mark]);
78
79        match args.get(placeholders) {
80            Some(RawArg::Value(v)) => w.push_arg(v.clone()),
81            Some(RawArg::Expr(e)) => w.write_expr(e),
82            Some(RawArg::Named(name)) => w.push_named_arg(name),
83            None => w.push_arg(Value::Null),
84        }
85
86        placeholders += 1;
87        rest = &rest[mark + 1..];
88    }
89
90    if placeholders != args.len() {
91        w.record_error(Error::raw_arg_count(placeholders, args.len(), sql));
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::dialect::testing::{Numbered, TestDialect};
99    use crate::writer::build;
100
101    // None of the cases in this module is judged by a grammar or an engine, and the
102    // reason is the subject matter rather than a missing dependency.
103    //
104    // `write_template` is a byte scan over text the author supplied. What it has to
105    // get right is where a `?` becomes a placeholder and where a `\?` collapses to
106    // a literal one — over inputs like `a\?b?c`, `\?\?` and `名前 = ?`, which are
107    // not SQL and were never meant to be. Wrapping them in a statement would put
108    // the frame under test instead of the scan.
109    //
110    // The `bob_raw_test` module below is a second, stronger reason: those seven
111    // cases are bob's `expr/raw_test.go` reproduced case for case, down to the
112    // `alphabet` table and the `?N` dialect. Their value is that they are
113    // *unaltered*; renaming the table to one the shared schema has would make them
114    // no longer the thing they are here to be.
115    //
116    // Where a template's numbering meets real SQL — a template spliced into a
117    // statement, or one holding a built expression — that is checked in
118    // `keelson-core/tests/grammar_raw.rs`, which does go to the judges.
119
120    /// bob's `expr/raw_test.go`, which is also the source of the seven
121    /// dialect-agnostic golden cases. Its dialect writes `?N`, `:name` and
122    /// `"quoted"`, which is [`TestDialect`].
123    mod bob_raw_test {
124        use super::*;
125
126        #[test]
127        fn plain() {
128            let (sql, args) = build(
129                &TestDialect,
130                &Expr::template("SELECT a, b FROM alphabet", []),
131            )
132            .unwrap();
133            assert_eq!(sql, "SELECT a, b FROM alphabet");
134            assert!(args.is_empty());
135        }
136
137        #[test]
138        fn escaped_args() {
139            let e = Expr::template(
140                r#"SELECT a, b FROM "alphabet\?" WHERE c = ? AND d <= ?"#,
141                [RawArg::value(1i32), RawArg::value(2i32)],
142            );
143            let (sql, args) = build(&TestDialect, &e).unwrap();
144            assert_eq!(
145                sql,
146                r#"SELECT a, b FROM "alphabet?" WHERE c = ?1 AND d <= ?2"#
147            );
148            assert_eq!(args, vec![Value::I32(1), Value::I32(2)]);
149        }
150
151        #[test]
152        fn mismatched_args_and_placeholders() {
153            let e = Expr::template("SELECT a, b FROM alphabet WHERE c = ? AND d <= ?", []);
154            // The SQL is written in full before the count is checked, so the
155            // error can quote the clause.
156            let mut w = SqlWriter::new(&TestDialect);
157            w.write_expr(&e);
158            assert_eq!(
159                w.sql(),
160                "SELECT a, b FROM alphabet WHERE c = ?1 AND d <= ?2"
161            );
162            let err = w.finish().unwrap_err();
163            assert_eq!(
164                err.to_string(),
165                "Bad Statement: has 2 placeholders but 0 args: \
166                 SELECT a, b FROM alphabet WHERE c = ? AND d <= ?"
167            );
168        }
169
170        #[test]
171        fn numbered_args() {
172            let e = Expr::template(
173                "SELECT a, b FROM alphabet WHERE c = ? AND d <= ?",
174                [RawArg::value(1i32), RawArg::value(2i32)],
175            );
176            let (sql, args) = build(&TestDialect, &e).unwrap();
177            assert_eq!(sql, "SELECT a, b FROM alphabet WHERE c = ?1 AND d <= ?2");
178            assert_eq!(args, vec![Value::I32(1), Value::I32(2)]);
179        }
180
181        #[test]
182        fn expr_args() {
183            // One `?` inside parentheses the author wrote, filled by an argument
184            // list that expands to three placeholders.
185            let e = Expr::template(
186                "SELECT a, b FROM alphabet WHERE c IN (?) AND d <= ?",
187                [RawArg::expr(Expr::args([5i32, 6, 7])), RawArg::value(2i32)],
188            );
189            let (sql, args) = build(&TestDialect, &e).unwrap();
190            assert_eq!(
191                sql,
192                "SELECT a, b FROM alphabet WHERE c IN (?1, ?2, ?3) AND d <= ?4"
193            );
194            assert_eq!(
195                args,
196                vec![Value::I32(5), Value::I32(6), Value::I32(7), Value::I32(2)]
197            );
198        }
199
200        #[test]
201        fn expr_args_group() {
202            // Same, with the parentheses coming from the expression instead.
203            let e = Expr::template(
204                "SELECT a, b FROM alphabet WHERE c IN ? AND d <= ?",
205                [
206                    RawArg::expr(crate::expr::arg_group([5i32, 6, 7])),
207                    RawArg::value(2i32),
208                ],
209            );
210            let (sql, args) = build(&TestDialect, &e).unwrap();
211            assert_eq!(
212                sql,
213                "SELECT a, b FROM alphabet WHERE c IN (?1, ?2, ?3) AND d <= ?4"
214            );
215            assert_eq!(
216                args,
217                vec![Value::I32(5), Value::I32(6), Value::I32(7), Value::I32(2)]
218            );
219        }
220
221        #[test]
222        fn expr_args_quote() {
223            // An identifier binds nothing, so the `?` that follows it is still
224            // the first placeholder.
225            let e = Expr::template(
226                "SELECT a, b FROM alphabet WHERE c = ? AND d <= ?",
227                [RawArg::expr(Expr::ident("AA")), RawArg::value(2i32)],
228            );
229            let (sql, args) = build(&TestDialect, &e).unwrap();
230            assert_eq!(
231                sql,
232                r#"SELECT a, b FROM alphabet WHERE c = "AA" AND d <= ?1"#
233            );
234            assert_eq!(args, vec![Value::I32(2)]);
235        }
236    }
237
238    #[test]
239    fn a_lone_escape_before_a_real_placeholder() {
240        let e = Expr::template(r"a\?b?c", [RawArg::value(1i32)]);
241        let (sql, _) = build(&Numbered, &e).unwrap();
242        assert_eq!(sql, "a?b$1c");
243    }
244
245    #[test]
246    fn an_escape_after_a_real_placeholder_is_still_honoured() {
247        let e = Expr::template(r"a?b\?c", [RawArg::value(1i32)]);
248        let (sql, _) = build(&Numbered, &e).unwrap();
249        assert_eq!(sql, "a$1b?c");
250    }
251
252    #[test]
253    fn consecutive_escapes_stay_literal() {
254        let e = Expr::template(r"\?\?", []);
255        let (sql, args) = build(&Numbered, &e).unwrap();
256        assert_eq!(sql, "??");
257        assert!(args.is_empty());
258    }
259
260    #[test]
261    fn a_trailing_placeholder_ends_the_scan() {
262        let e = Expr::template("a = ?", [RawArg::value(9i32)]);
263        assert_eq!(build(&Numbered, &e).unwrap().0, "a = $1");
264    }
265
266    #[test]
267    fn too_many_args_is_also_a_mismatch() {
268        let e = Expr::template("a = ?", [RawArg::value(1i32), RawArg::value(2i32)]);
269        let err = build(&Numbered, &e).unwrap_err();
270        assert_eq!(
271            err.to_string(),
272            "Bad Statement: has 1 placeholders but 2 args: a = ?"
273        );
274    }
275
276    #[test]
277    fn a_named_replacement_consumes_a_placeholder_but_no_position() {
278        let e = Expr::template(
279            "a = ? AND b = ? AND c = ?",
280            [RawArg::value(1i32), RawArg::named("b"), RawArg::value(3i32)],
281        );
282        let (sql, args) = build(&TestDialect, &e).unwrap();
283        assert_eq!(sql, "a = ?1 AND b = :b AND c = ?2");
284        assert_eq!(args, vec![Value::I32(1), Value::I32(3)]);
285    }
286
287    #[test]
288    fn a_named_replacement_fails_on_a_dialect_without_named_arguments() {
289        let e = Expr::template("a = ?", [RawArg::named("a")]);
290        assert!(matches!(build(&Numbered, &e), Err(Error::NoNamedArgs)));
291    }
292
293    #[test]
294    fn multibyte_text_around_a_placeholder_is_not_sliced_mid_character() {
295        let e = Expr::template(
296            "名前 = ? AND 年齢 > ?",
297            [RawArg::value("さくら"), RawArg::value(20i32)],
298        );
299        let (sql, _) = build(&Numbered, &e).unwrap();
300        assert_eq!(sql, "名前 = $1 AND 年齢 > $2");
301    }
302
303    #[test]
304    fn a_template_continues_the_surrounding_numbering() {
305        let outer = Expr::join([
306            Expr::arg(1i32),
307            Expr::template("f(?, ?)", [RawArg::value(2i32), RawArg::value(3i32)]),
308            Expr::arg(4i32),
309        ]);
310        let (sql, args) = build(&Numbered, &outer).unwrap();
311        assert_eq!(sql, "$1 f($2, $3) $4");
312        assert_eq!(args.len(), 4);
313    }
314}