Skip to main content

keelson_sqlite/
dialect.rs

1use keelson_core::{Dialect, SqlWriter};
2
3/// The SQLite dialect: `?1` placeholders, `:name` named arguments, `"` quoting.
4///
5/// SQLite is the one dialect keelson targets that has *both* numbered positional
6/// placeholders and named ones (<https://www.sqlite.org/lang_expr.html#varparam>),
7/// so it is the only [`Dialect`] that overrides
8/// [`write_named_arg`](Dialect::write_named_arg). `?NNN` is used rather than a bare
9/// `?` because SQLite numbers an unadorned `?` implicitly and mixing the two forms
10/// is how a statement quietly binds the wrong argument.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
12pub struct Sqlite;
13
14impl Dialect for Sqlite {
15    fn write_arg(&self, w: &mut SqlWriter<'_>, position: usize) {
16        w.push_str("?");
17        w.push_str(&position.to_string());
18    }
19
20    /// Quote `s` as a delimited identifier.
21    ///
22    /// An embedded `"` is doubled. SQLite accepts `[x]` and `` `x` `` as well, for
23    /// compatibility with SQL Server and MySQL, but `"x"` is the standard form and
24    /// the only one this dialect writes.
25    fn write_quoted(&self, w: &mut SqlWriter<'_>, s: &str) {
26        w.push_str("\"");
27        if s.contains('"') {
28            w.push_str(&s.replace('"', "\"\""));
29        } else {
30            w.push_str(s);
31        }
32        w.push_str("\"");
33    }
34
35    fn write_named_arg(&self, w: &mut SqlWriter<'_>, name: &str) {
36        w.push_str(":");
37        w.push_str(name);
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use keelson_core::{Value, build, expr};
45
46    #[test]
47    fn placeholders_are_numbered_and_identifiers_are_double_quoted() {
48        let e = expr::Expr::join((expr::quote(("users", "id")), expr::arg(1i32)));
49        let (sql, args) = build(&Sqlite, &e).unwrap();
50        assert_eq!(sql, r#""users"."id" ?1"#);
51        assert_eq!(args, vec![Value::I32(1)]);
52    }
53
54    #[test]
55    fn an_embedded_quote_is_doubled() {
56        let (sql, _) = build(&Sqlite, &expr::quote(r#"we"ird"#)).unwrap();
57        assert_eq!(sql, r#""we""ird""#);
58    }
59
60    /// The one dialect that has them. A named argument binds nothing and does not
61    /// advance the positional counter.
62    #[test]
63    fn named_arguments_are_supported_and_consume_no_position() {
64        let e = expr::Expr::join((expr::arg(1i32), expr::named("cutoff"), expr::arg(2i32)));
65        let (sql, args) = build(&Sqlite, &e).unwrap();
66        assert_eq!(sql, "?1 :cutoff ?2");
67        assert_eq!(args, vec![Value::I32(1), Value::I32(2)]);
68    }
69}