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}