Skip to main content

keelson_psql/
dialect.rs

1use keelson_core::{Dialect, SqlWriter};
2
3/// The PostgreSQL dialect: `$1` placeholders, `"` quoting, no named arguments.
4///
5/// A unit struct rather than a value to be constructed, because there is nothing
6/// to configure — every query type hands out `&Psql` from
7/// [`Query::dialect`](keelson_core::Query::dialect).
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
9pub struct Psql;
10
11impl Dialect for Psql {
12    fn write_arg(&self, w: &mut SqlWriter<'_>, position: usize) {
13        w.push_str("$");
14        w.push_str(&position.to_string());
15    }
16
17    /// Quote `s` as a delimited identifier.
18    ///
19    /// An embedded `"` is doubled, which is how PostgreSQL escapes one inside a
20    /// delimited identifier (`4.1.1 Identifiers and Key Words`). bob writes the
21    /// name through unedited, so a name containing a quote silently ends the
22    /// identifier there; the fast path here is the same single `push_str` for the
23    /// overwhelmingly common case, and the scan only pays for itself when a quote
24    /// is actually present.
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
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use keelson_core::{Value, build, expr};
40
41    #[test]
42    fn placeholders_are_dollar_numbered_and_identifiers_are_double_quoted() {
43        let e = expr::Expr::join((expr::quote(("users", "id")), expr::arg(1i32)));
44        let (sql, args) = build(&Psql, &e).unwrap();
45        assert_eq!(sql, r#""users"."id" $1"#);
46        assert_eq!(args, vec![Value::I32(1)]);
47    }
48
49    #[test]
50    fn an_embedded_quote_is_doubled() {
51        let (sql, _) = build(&Psql, &expr::quote(r#"we"ird"#)).unwrap();
52        assert_eq!(sql, r#""we""ird""#);
53    }
54
55    #[test]
56    fn named_arguments_are_refused() {
57        assert!(matches!(
58            build(&Psql, &expr::named("x")),
59            Err(keelson_core::Error::NoNamedArgs)
60        ));
61    }
62}