Skip to main content

keelson_core/
dialect.rs

1use crate::error::Error;
2use crate::writer::SqlWriter;
3
4/// The per-database syntax an expression needs in order to render itself.
5///
6/// The three dialects keelson targets differ only in these decisions:
7///
8/// | dialect | placeholder | quote | named |
9/// |---|---|---|---|
10/// | PostgreSQL | `$1` | `"id"` | unsupported |
11/// | MySQL | `?` (position ignored) | `` `id` `` | unsupported |
12/// | SQLite | `?1` | `"id"` | `:name` |
13///
14/// bob splits named-argument support into a second `DialectWithNamed` interface
15/// and type-asserts on it. We keep one trait with a defaulted method instead, so
16/// resolution stays static and "this dialect cannot do that" is one recorded
17/// error rather than a failed downcast.
18///
19/// All three methods are infallible. They append to the writer's buffer, and
20/// writing into a `String` cannot fail; the one real failure — a named argument
21/// asked of a dialect that has none — is *recorded* on the writer with
22/// [`SqlWriter::record_error`] and surfaced later by [`build`](crate::build).
23///
24/// # Implementing
25///
26/// Write only through [`SqlWriter::push_str`]. Never call
27/// [`SqlWriter::push_arg`] from inside [`write_arg`](Self::write_arg): that is
28/// the method `push_arg` calls, and it is the only place the placeholder counter
29/// advances.
30pub trait Dialect: std::fmt::Debug + Send + Sync {
31    /// Write the placeholder for the argument at `position` (1-based).
32    ///
33    /// A dialect with positional placeholders ignores `position` — the argument
34    /// order carries the meaning instead.
35    fn write_arg(&self, w: &mut SqlWriter<'_>, position: usize);
36
37    /// Write `s` as a quoted identifier, including the quote characters.
38    fn write_quoted(&self, w: &mut SqlWriter<'_>, s: &str);
39
40    /// Write the placeholder for a named argument.
41    ///
42    /// Defaults to recording [`Error::NoNamedArgs`]; only SQLite overrides it.
43    fn write_named_arg(&self, w: &mut SqlWriter<'_>, _name: &str) {
44        w.record_error(Error::NoNamedArgs);
45    }
46}
47
48impl<D: Dialect + ?Sized> Dialect for &D {
49    fn write_arg(&self, w: &mut SqlWriter<'_>, position: usize) {
50        (**self).write_arg(w, position);
51    }
52
53    fn write_quoted(&self, w: &mut SqlWriter<'_>, s: &str) {
54        (**self).write_quoted(w, s);
55    }
56
57    fn write_named_arg(&self, w: &mut SqlWriter<'_>, name: &str) {
58        (**self).write_named_arg(w, name);
59    }
60}
61
62/// Stand-in dialects, for tests and for the dialect-agnostic golden cases.
63///
64/// Enabled by this crate's own tests and by the `testing` feature, which the
65/// dialect crates switch on as a dev-dependency. The real dialects ship in
66/// `keelson-psql`, `keelson-mysql` and `keelson-sqlite`; these exist so that
67/// expression-level code can be tested without depending on any of them.
68#[cfg(any(test, feature = "testing"))]
69pub mod testing {
70    use super::Dialect;
71    use crate::writer::SqlWriter;
72
73    /// `?1` placeholders, `:name` named arguments and `"` quoting.
74    ///
75    /// Byte-for-byte the dialect bob's own `expr` package tests with, which is
76    /// what the dialect-agnostic golden cases were recorded against. Use this one
77    /// unless a test is specifically about placeholder style.
78    #[derive(Debug, Clone, Copy, Default)]
79    pub struct TestDialect;
80
81    impl Dialect for TestDialect {
82        fn write_arg(&self, w: &mut SqlWriter<'_>, position: usize) {
83            w.push_str("?");
84            w.push_str(&position.to_string());
85        }
86
87        fn write_quoted(&self, w: &mut SqlWriter<'_>, s: &str) {
88            w.push_str("\"");
89            w.push_str(s);
90            w.push_str("\"");
91        }
92
93        fn write_named_arg(&self, w: &mut SqlWriter<'_>, name: &str) {
94            w.push_str(":");
95            w.push_str(name);
96        }
97    }
98
99    /// `$1` placeholders and `"` quoting, like PostgreSQL. No named arguments.
100    #[derive(Debug, Clone, Copy, Default)]
101    pub struct Numbered;
102
103    impl Dialect for Numbered {
104        fn write_arg(&self, w: &mut SqlWriter<'_>, position: usize) {
105            w.push_str("$");
106            w.push_str(&position.to_string());
107        }
108
109        fn write_quoted(&self, w: &mut SqlWriter<'_>, s: &str) {
110            w.push_str("\"");
111            w.push_str(s);
112            w.push_str("\"");
113        }
114    }
115
116    /// `?` placeholders and backtick quoting, like MySQL.
117    ///
118    /// The position is dropped, which is exactly what makes a re-indexing bug
119    /// invisible there — so indexing tests use [`Numbered`] instead.
120    #[derive(Debug, Clone, Copy, Default)]
121    pub struct Positional;
122
123    impl Dialect for Positional {
124        fn write_arg(&self, w: &mut SqlWriter<'_>, _position: usize) {
125            w.push_str("?");
126        }
127
128        fn write_quoted(&self, w: &mut SqlWriter<'_>, s: &str) {
129            w.push_str("`");
130            w.push_str(s);
131            w.push_str("`");
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::testing::{Numbered, Positional, TestDialect};
139    use super::*;
140    use crate::error::Error;
141
142    // These cases are about the three `Dialect` methods and nothing else, so what
143    // they assert is a placeholder and a quoted identifier written back to back —
144    // `$1"id"`. That is deliberately not SQL: the point is which characters each
145    // dialect emits, and a statement would only hide them behind a frame. The
146    // dialects' output *as SQL* is judged wherever a fragment or statement rendered
147    // through them is, which is most of the rest of this crate's tests.
148
149    /// The recorded shapes of the three real dialects, checked against the ones
150    /// this trait has to be able to express.
151    #[test]
152    fn the_trait_expresses_all_three_real_dialects() {
153        // psql: $N and "id"
154        let mut w = SqlWriter::new(&Numbered);
155        w.push_arg(1i32);
156        w.push_quoted(&["id"]);
157        assert_eq!(w.sql(), r#"$1"id""#);
158
159        // mysql: ? and `id`
160        let mut w = SqlWriter::new(&Positional);
161        w.push_arg(1i32);
162        w.push_quoted(&["id"]);
163        assert_eq!(w.sql(), "?`id`");
164
165        // sqlite: ?N, :name and "id"
166        let mut w = SqlWriter::new(&TestDialect);
167        w.push_arg(1i32);
168        w.push_named_arg("name");
169        w.push_quoted(&["id"]);
170        assert_eq!(w.sql(), r#"?1:name"id""#);
171    }
172
173    #[test]
174    fn named_args_are_refused_unless_the_dialect_opts_in() {
175        for d in [&Numbered as &dyn Dialect, &Positional] {
176            let mut w = SqlWriter::new(d);
177            w.push_named_arg("id");
178            assert!(matches!(w.error(), Some(Error::NoNamedArgs)));
179        }
180    }
181
182    #[test]
183    fn a_dialect_reference_is_itself_a_dialect() {
184        // So that `impl Dialect for X` is usable both as `&X` and `&&X`.
185        let d = &Numbered;
186        let mut w = SqlWriter::new(&d);
187        w.push_arg(7i32);
188        assert_eq!(w.sql(), "$1");
189    }
190}