1#![allow(dead_code)] use super::ir::{JsonType, SqlType};
12
13pub trait Dialect: Send + Sync {
15 fn name(&self) -> &'static str;
17
18 fn placeholder(&self, idx: usize) -> String;
20
21 fn json_field(&self, base: &str, key: &str) -> String;
23
24 fn json_field_text(&self, base: &str, key: &str) -> String;
26
27 fn json_path(&self, base: &str, segments: &[&str]) -> String;
29
30 fn json_path_text(&self, base: &str, segments: &[&str]) -> String;
32
33 fn unnest_array(&self, expr: &str) -> String;
36
37 fn coalesce_array(&self, expr: &str) -> String;
41
42 fn json_type(&self, expr: &str) -> String;
45
46 fn json_agg(&self, expr: &str) -> String;
48
49 fn string_agg(&self, expr: &str, sep_param: &str) -> String;
51
52 fn bool_true(&self) -> &'static str;
54 fn bool_false(&self) -> &'static str;
56
57 fn lateral_keyword(&self) -> &'static str;
59
60 fn cast(&self, inner: &str, ty: SqlType) -> String;
62
63 fn has_json_type(&self, expr: &str, ty: JsonType) -> String;
65
66 fn truthy_predicate(&self, expr: &str) -> String;
72
73 fn last_path_segment(&self, s: &str) -> String;
77}
78
79#[derive(Debug, Default, Clone, Copy)]
85pub struct PgDialect;
86
87impl Dialect for PgDialect {
88 fn name(&self) -> &'static str {
89 "postgres"
90 }
91
92 fn placeholder(&self, idx: usize) -> String {
93 format!("${idx}")
94 }
95
96 fn json_field(&self, base: &str, key: &str) -> String {
97 format!("{base}->'{key}'")
98 }
99
100 fn json_field_text(&self, base: &str, key: &str) -> String {
101 format!("{base}->>'{key}'")
102 }
103
104 fn json_path(&self, base: &str, segments: &[&str]) -> String {
105 if segments.len() == 1 {
106 self.json_field(base, segments[0])
107 } else {
108 format!("{base}#>'{{{}}}'", segments.join(","))
109 }
110 }
111
112 fn json_path_text(&self, base: &str, segments: &[&str]) -> String {
113 if segments.len() == 1 {
114 self.json_field_text(base, segments[0])
115 } else {
116 format!("{base}#>>'{{{}}}'", segments.join(","))
117 }
118 }
119
120 fn unnest_array(&self, expr: &str) -> String {
121 format!("jsonb_array_elements({expr})")
122 }
123
124 fn coalesce_array(&self, expr: &str) -> String {
125 format!("coalesce({expr}, '[]'::jsonb)")
126 }
127
128 fn json_type(&self, expr: &str) -> String {
129 format!("jsonb_typeof({expr})")
130 }
131
132 fn json_agg(&self, expr: &str) -> String {
133 format!("coalesce(jsonb_agg({expr}), '[]'::jsonb)")
138 }
139
140 fn string_agg(&self, expr: &str, sep_param: &str) -> String {
141 format!("string_agg({expr}, {sep_param})")
142 }
143
144 fn bool_true(&self) -> &'static str {
145 "true"
146 }
147
148 fn bool_false(&self) -> &'static str {
149 "false"
150 }
151
152 fn lateral_keyword(&self) -> &'static str {
153 "LATERAL "
154 }
155
156 fn cast(&self, inner: &str, ty: SqlType) -> String {
157 match ty {
158 SqlType::Text => format!("({inner})::text"),
159 SqlType::Integer => format!("(({inner})::bigint)::text"),
166 SqlType::Decimal => format!("(({inner})::numeric)::text"),
167 SqlType::Boolean => {
175 format!(
176 "CASE WHEN ({inner})::boolean IS TRUE THEN 'true' \
177 WHEN ({inner})::boolean IS FALSE THEN 'false' END"
178 )
179 }
180 SqlType::Json => format!("({inner})::jsonb"),
181 }
182 }
183
184 fn has_json_type(&self, expr: &str, ty: JsonType) -> String {
185 let name = match ty {
186 JsonType::Object => "object",
187 JsonType::Array => "array",
188 JsonType::String => "string",
189 JsonType::Number => "number",
190 JsonType::Boolean => "boolean",
191 JsonType::Null => "null",
192 };
193 format!("jsonb_typeof({expr}) = '{name}'")
194 }
195
196 fn truthy_predicate(&self, expr: &str) -> String {
197 format!("({expr})::boolean IS TRUE")
201 }
202
203 fn last_path_segment(&self, s: &str) -> String {
204 format!("regexp_replace({s}, '.*/', '')")
206 }
207}
208
209#[derive(Debug, Default, Clone, Copy)]
215pub struct SqliteDialect;
216
217impl Dialect for SqliteDialect {
218 fn name(&self) -> &'static str {
219 "sqlite"
220 }
221
222 fn placeholder(&self, idx: usize) -> String {
223 format!("?{idx}")
224 }
225
226 fn json_field(&self, base: &str, key: &str) -> String {
227 format!("json_extract({base}, '$.{key}')")
228 }
229
230 fn json_field_text(&self, base: &str, key: &str) -> String {
231 self.json_field(base, key)
235 }
236
237 fn json_path(&self, base: &str, segments: &[&str]) -> String {
238 let mut path = String::from("$");
242 for seg in segments {
243 if seg.chars().all(|c| c.is_ascii_digit()) {
244 path.push('[');
245 path.push_str(seg);
246 path.push(']');
247 } else {
248 path.push('.');
249 path.push_str(seg);
250 }
251 }
252 format!("json_extract({base}, '{path}')")
253 }
254
255 fn json_path_text(&self, base: &str, segments: &[&str]) -> String {
256 self.json_path(base, segments)
257 }
258
259 fn unnest_array(&self, expr: &str) -> String {
260 format!("json_each({expr})")
261 }
262
263 fn coalesce_array(&self, expr: &str) -> String {
264 format!("coalesce({expr}, '[]')")
265 }
266
267 fn json_type(&self, expr: &str) -> String {
268 format!("json_type({expr})")
269 }
270
271 fn json_agg(&self, expr: &str) -> String {
272 format!("json_group_array({expr})")
273 }
274
275 fn string_agg(&self, expr: &str, sep_param: &str) -> String {
276 format!("group_concat({expr}, {sep_param})")
277 }
278
279 fn bool_true(&self) -> &'static str {
280 "1"
281 }
282
283 fn bool_false(&self) -> &'static str {
284 "0"
285 }
286
287 fn lateral_keyword(&self) -> &'static str {
288 ""
289 }
290
291 fn cast(&self, inner: &str, ty: SqlType) -> String {
292 match ty {
293 SqlType::Text => format!("CAST({inner} AS TEXT)"),
294 SqlType::Integer => format!("CAST({inner} AS INTEGER)"),
295 SqlType::Decimal => format!("CAST({inner} AS REAL)"),
296 SqlType::Boolean => {
300 format!("CASE WHEN ({inner}) THEN 'true' WHEN NOT ({inner}) THEN 'false' END")
301 }
302 SqlType::Json => format!("json({inner})"),
303 }
304 }
305
306 fn has_json_type(&self, expr: &str, ty: JsonType) -> String {
307 let name = match ty {
308 JsonType::Object => "object",
309 JsonType::Array => "array",
310 JsonType::String => "text",
311 JsonType::Number => "integer", JsonType::Boolean => "true", JsonType::Null => "null",
314 };
315 format!("json_type({expr}) = '{name}'")
316 }
317
318 fn truthy_predicate(&self, expr: &str) -> String {
319 format!("({expr}) IS NOT NULL AND ({expr}) != 0 AND ({expr}) != 'false'")
324 }
325
326 fn last_path_segment(&self, s: &str) -> String {
327 format!("fhir_last_segment({s})")
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn pg_field_text() {
340 assert_eq!(PgDialect.json_field_text("r.data", "id"), "r.data->>'id'");
341 }
342
343 #[test]
344 fn pg_path_text_dotted() {
345 assert_eq!(
346 PgDialect.json_path_text("r.data", &["subject", "reference"]),
347 "r.data#>>'{subject,reference}'"
348 );
349 }
350
351 #[test]
352 fn sqlite_field() {
353 assert_eq!(
354 SqliteDialect.json_field("r.data", "id"),
355 "json_extract(r.data, '$.id')"
356 );
357 }
358
359 #[test]
360 fn sqlite_path_dotted() {
361 assert_eq!(
362 SqliteDialect.json_path("r.data", &["subject", "reference"]),
363 "json_extract(r.data, '$.subject.reference')"
364 );
365 }
366
367 #[test]
368 fn placeholder_forms() {
369 assert_eq!(PgDialect.placeholder(3), "$3");
370 assert_eq!(SqliteDialect.placeholder(3), "?3");
371 }
372}