1use dbrest_core::backend::SqlDialect;
4use dbrest_core::plan::types::CoercibleField;
5use dbrest_core::query::sql_builder::{SqlBuilder, SqlParam};
6
7#[derive(Debug, Clone, Copy)]
9pub struct SqliteDialect;
10
11impl SqlDialect for SqliteDialect {
12 fn json_agg_with_columns(&self, b: &mut SqlBuilder, alias: &str, columns: &[&str]) {
13 if columns.is_empty() {
14 b.push("COALESCE(json_group_array(json(");
16 b.push_ident(alias);
17 b.push(")), '[]')");
18 } else {
19 b.push("COALESCE(json_group_array(json_object(");
21 for (i, col) in columns.iter().enumerate() {
22 if i > 0 {
23 b.push(", ");
24 }
25 b.push("'");
26 b.push(col);
27 b.push("', ");
28 b.push_ident(alias);
29 b.push(".");
30 b.push_ident(col);
31 }
32 b.push(")), '[]')");
33 }
34 }
35
36 fn row_to_json_with_columns(&self, b: &mut SqlBuilder, alias: &str, columns: &[&str]) {
37 if columns.is_empty() {
38 b.push("json(");
39 b.push_ident(alias);
40 b.push(")");
41 } else {
42 b.push("json_object(");
44 for (i, col) in columns.iter().enumerate() {
45 if i > 0 {
46 b.push(", ");
47 }
48 b.push("'");
49 b.push(col);
50 b.push("', ");
51 b.push_ident(alias);
52 b.push(".");
53 b.push_ident(col);
54 }
55 b.push(")");
56 }
57 }
58
59 fn count_expr(&self, b: &mut SqlBuilder, expr: &str) {
60 b.push("COUNT(");
61 b.push_ident(expr);
62 b.push(")");
63 }
64
65 fn count_star(&self, b: &mut SqlBuilder) {
66 b.push("SELECT COUNT(*) AS ");
67 b.push_ident("dbrst_filtered_count");
68 }
69
70 fn set_session_var(&self, _b: &mut SqlBuilder, _key: &str, _value: &str) {
71 tracing::warn!(
72 "set_session_var called on SqliteDialect — this is a no-op; \
73 use build_tx_vars_statement instead"
74 );
75 }
76
77 fn session_vars_are_select_exprs(&self) -> bool {
78 false
79 }
80
81 fn build_tx_vars_statement(&self, b: &mut SqlBuilder, vars: &[(&str, &str)]) {
82 b.push("INSERT OR REPLACE INTO _dbrest_vars(key, val) VALUES ");
84 for (i, (key, value)) in vars.iter().enumerate() {
85 if i > 0 {
86 b.push(", ");
87 }
88 b.push("('");
89 for ch in key.chars() {
90 if ch == '\'' {
91 b.push("'");
92 }
93 b.push_char(ch);
94 }
95 b.push("', '");
96 for ch in value.chars() {
97 if ch == '\'' {
98 b.push("'");
99 }
100 b.push_char(ch);
101 }
102 b.push("')");
103 }
104 }
105
106 fn get_session_var(&self, b: &mut SqlBuilder, key: &str, column_alias: &str) {
107 b.push("(SELECT val FROM _dbrest_vars WHERE key = '");
109 for ch in key.chars() {
110 if ch == '\'' {
111 b.push("'");
112 }
113 b.push_char(ch);
114 }
115 b.push("') AS ");
116 b.push(column_alias);
117 }
118
119 fn type_cast(&self, b: &mut SqlBuilder, expr: &str, ty: &str) {
120 b.push("CAST(");
121 b.push(expr);
122 b.push(" AS ");
123 b.push(&sqlite_type(ty));
124 b.push(")");
125 }
126
127 fn from_json_body(&self, b: &mut SqlBuilder, columns: &[CoercibleField], json_bytes: &[u8]) {
128 let is_array = json_bytes.first().map(|&c| c == b'[').unwrap_or(false);
133
134 if is_array {
135 b.push("(SELECT ");
136 b.push_separated(", ", columns, |b, col| {
137 b.push("json_extract(value, '$.");
138 b.push(&col.name.replace('\'', "''"));
140 b.push("') AS ");
141 b.push_ident(&col.name);
142 });
143 b.push(" FROM json_each(");
144 b.push_param(SqlParam::Text(
145 String::from_utf8_lossy(json_bytes).into_owned(),
146 ));
147 b.push("))");
148 } else {
149 b.push("(SELECT ");
151 b.push_separated(", ", columns, |b, col| {
152 b.push("json_extract(");
153 b.push_param(SqlParam::Text(
154 String::from_utf8_lossy(json_bytes).into_owned(),
155 ));
156 b.push(", '$.");
157 b.push(&col.name.replace('\'', "''"));
158 b.push("') AS ");
159 b.push_ident(&col.name);
160 });
161 b.push(")");
162 }
163 }
164
165 fn push_type_cast_suffix(&self, b: &mut SqlBuilder, ty: &str) {
166 let _ = (b, ty);
170 }
171
172 fn push_array_type_cast_suffix(&self, b: &mut SqlBuilder, _ty: &str) {
173 let _ = b;
175 }
176
177 fn quote_ident(&self, ident: &str) -> String {
178 format!("\"{}\"", ident.replace('"', "\"\""))
179 }
180
181 fn quote_literal(&self, lit: &str) -> String {
182 format!("'{}'", lit.replace('\'', "''"))
183 }
184
185 fn supports_fts(&self) -> bool {
186 false }
188
189 fn fts_predicate(
190 &self,
191 _b: &mut SqlBuilder,
192 _config: Option<&str>,
193 _column: &str,
194 _operator: &str,
195 ) {
196 }
199
200 fn row_to_json_star(&self, b: &mut SqlBuilder, source: &str) {
201 b.push("json_group_array(json_object(*)) FROM ");
205 b.push(source);
206 }
207
208 fn count_star_from(&self, b: &mut SqlBuilder, source: &str) {
209 b.push("(SELECT COUNT(*) FROM ");
210 b.push(source);
211 b.push(")");
212 }
213
214 fn push_literal(&self, b: &mut SqlBuilder, s: &str) {
215 b.push("'");
217 for ch in s.chars() {
218 if ch == '\'' {
219 b.push("'");
220 }
221 b.push_char(ch);
222 }
223 b.push("'");
224 }
225
226 fn supports_lateral_join(&self) -> bool {
227 false
228 }
229
230 fn named_param_assign(&self) -> &str {
231 " = "
234 }
235
236 fn supports_dml_cte(&self) -> bool {
237 false
238 }
239}
240
241fn sqlite_type(pg_type: &str) -> String {
243 match pg_type.to_lowercase().as_str() {
244 "integer" | "int" | "int4" | "int8" | "bigint" | "smallint" | "int2" | "serial"
245 | "bigserial" => "INTEGER".to_string(),
246 "real" | "float4" | "float8" | "double precision" | "numeric" | "decimal" => {
247 "REAL".to_string()
248 }
249 "boolean" | "bool" => "INTEGER".to_string(), "blob" | "bytea" => "BLOB".to_string(),
251 "json" | "jsonb" => "TEXT".to_string(),
252 _ => "TEXT".to_string(),
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 fn dialect() -> SqliteDialect {
261 SqliteDialect
262 }
263
264 #[test]
265 fn test_json_agg() {
266 let mut b = SqlBuilder::new();
267 dialect().json_agg(&mut b, "_dbrst_t");
268 assert_eq!(
269 b.sql(),
270 "COALESCE(json_group_array(json(\"_dbrst_t\")), '[]')"
271 );
272 }
273
274 #[test]
275 fn test_count_expr() {
276 let mut b = SqlBuilder::new();
277 dialect().count_expr(&mut b, "_dbrst_t");
278 assert_eq!(b.sql(), "COUNT(\"_dbrst_t\")");
279 }
280
281 #[test]
282 fn test_count_star_from() {
283 let mut b = SqlBuilder::new();
284 dialect().count_star_from(&mut b, "dbrst_source");
285 assert_eq!(b.sql(), "(SELECT COUNT(*) FROM dbrst_source)");
286 }
287
288 #[test]
289 fn test_type_cast() {
290 let mut b = SqlBuilder::new();
291 dialect().type_cast(&mut b, "col", "integer");
292 assert_eq!(b.sql(), "CAST(col AS INTEGER)");
293 }
294
295 #[test]
296 fn test_type_cast_text() {
297 let mut b = SqlBuilder::new();
298 dialect().type_cast(&mut b, "col", "varchar");
299 assert_eq!(b.sql(), "CAST(col AS TEXT)");
300 }
301
302 #[test]
303 fn test_push_literal_no_backslash() {
304 let mut b = SqlBuilder::new();
305 dialect().push_literal(&mut b, "hello");
306 assert_eq!(b.sql(), "'hello'");
307 }
308
309 #[test]
310 fn test_push_literal_with_quote() {
311 let mut b = SqlBuilder::new();
312 dialect().push_literal(&mut b, "it's");
313 assert_eq!(b.sql(), "'it''s'");
314 }
315
316 #[test]
317 fn test_push_literal_with_backslash() {
318 let mut b = SqlBuilder::new();
319 dialect().push_literal(&mut b, "back\\slash");
320 assert_eq!(b.sql(), "'back\\slash'");
322 }
323
324 #[test]
325 fn test_build_tx_vars_statement() {
326 let mut b = SqlBuilder::new();
327 dialect().build_tx_vars_statement(
328 &mut b,
329 &[("request.method", "GET"), ("request.path", "/users")],
330 );
331 let sql = b.sql();
332 assert!(sql.contains("INSERT OR REPLACE INTO _dbrest_vars"));
333 assert!(sql.contains("request.method"));
334 assert!(sql.contains("GET"));
335 assert!(sql.contains("request.path"));
336 assert!(sql.contains("/users"));
337 }
338
339 #[test]
340 fn test_session_vars_are_not_select_exprs() {
341 assert!(!dialect().session_vars_are_select_exprs());
342 }
343
344 #[test]
345 fn test_get_session_var() {
346 let mut b = SqlBuilder::new();
347 dialect().get_session_var(&mut b, "response.headers", "response_headers");
348 assert!(b.sql().contains("_dbrest_vars"));
349 assert!(b.sql().contains("response.headers"));
350 assert!(b.sql().contains("AS response_headers"));
351 }
352
353 #[test]
354 fn test_quote_ident() {
355 assert_eq!(dialect().quote_ident("users"), "\"users\"");
356 assert_eq!(dialect().quote_ident("a\"b"), "\"a\"\"b\"");
357 }
358
359 #[test]
360 fn test_quote_literal() {
361 assert_eq!(dialect().quote_literal("hello"), "'hello'");
362 assert_eq!(dialect().quote_literal("it's"), "'it''s'");
363 }
364
365 #[test]
366 fn test_supports_fts() {
367 assert!(!dialect().supports_fts());
368 }
369
370 #[test]
371 fn test_supports_lateral_join() {
372 assert!(!dialect().supports_lateral_join());
373 }
374
375 #[test]
376 fn test_set_session_var_does_not_panic() {
377 let d = dialect();
378 let mut b = SqlBuilder::new();
379 d.set_session_var(&mut b, "key", "value");
381 assert_eq!(b.sql(), "");
382 }
383
384 #[test]
385 fn test_sqlite_type_mapping() {
386 assert_eq!(sqlite_type("integer"), "INTEGER");
387 assert_eq!(sqlite_type("bigint"), "INTEGER");
388 assert_eq!(sqlite_type("real"), "REAL");
389 assert_eq!(sqlite_type("float8"), "REAL");
390 assert_eq!(sqlite_type("boolean"), "INTEGER");
391 assert_eq!(sqlite_type("bytea"), "BLOB");
392 assert_eq!(sqlite_type("json"), "TEXT");
393 assert_eq!(sqlite_type("text"), "TEXT");
394 assert_eq!(sqlite_type("varchar"), "TEXT");
395 }
396}