fraiseql_db/dialect/trait_def.rs
1//! `SqlDialect` trait — dialect-specific SQL rendering primitives.
2
3use std::borrow::Cow;
4
5/// Column type used by row-shaped view (`vr_*`) DDL generation.
6///
7/// Maps GraphQL scalar types to their SQL equivalents for typed column
8/// extraction from JSON/JSONB data columns.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum RowViewColumnType {
11 /// Text / varchar column.
12 Text,
13 /// 32-bit integer.
14 Int32,
15 /// 64-bit integer.
16 Int64,
17 /// 64-bit floating point.
18 Float64,
19 /// Boolean.
20 Boolean,
21 /// UUID.
22 Uuid,
23 /// Timestamp with timezone.
24 Timestamptz,
25 /// JSON / JSONB.
26 Json,
27 /// Date (without time).
28 Date,
29}
30
31/// Error returned when an operator is not supported by a dialect.
32#[derive(Debug)]
33pub struct UnsupportedOperator {
34 /// Dialect name (e.g., "MySQL").
35 pub dialect: &'static str,
36 /// Operator name (e.g., "ArrayContainedBy").
37 pub operator: &'static str,
38}
39
40impl std::fmt::Display for UnsupportedOperator {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(
43 f,
44 "operator `{}` is not supported by the {} dialect",
45 self.operator, self.dialect
46 )
47 }
48}
49
50impl std::error::Error for UnsupportedOperator {}
51
52/// Dialect-specific SQL rendering primitives for WHERE clause generation.
53///
54/// Implement this trait to add a new database backend. All methods that are
55/// identical across dialects have default implementations — override only
56/// what your dialect requires.
57///
58/// # Security contract
59///
60/// Implementations MUST:
61/// - Never interpolate user-supplied values into the returned SQL string. Use [`Self::placeholder`]
62/// and append values to the `params` vector instead.
63/// - Escape field / column names via the `path_escape` module.
64/// - Escape literal SQL identifiers (not values) by doubling the delimiter.
65pub trait SqlDialect: Send + Sync + 'static {
66 // ── Core primitives (must implement) ──────────────────────────────────────
67
68 /// Dialect name for error messages (e.g., `"PostgreSQL"`, `"MySQL"`).
69 fn name(&self) -> &'static str;
70
71 /// Quote a database identifier (table or column name).
72 ///
73 /// # Examples
74 /// - PostgreSQL: `v_user` → `"v_user"`, `evil"name` → `"evil""name"`
75 /// - MySQL: `v_user` → `` `v_user` ``, `` evil`name `` → `` `evil``name` ``
76 /// - SQL Server: `v_user` → `[v_user]`, `evil]name` → `[evil]]name]`
77 fn quote_identifier(&self, name: &str) -> String;
78
79 /// Generate SQL to extract a scalar string from a JSON/JSONB column.
80 ///
81 /// `column` is the unquoted column name (typically `"data"`).
82 /// `path` is the slice of field-name segments (pre-escaped by caller if needed).
83 ///
84 /// # Examples
85 /// - PostgreSQL (1 segment): `data->>'field'`
86 /// - MySQL: `JSON_UNQUOTE(JSON_EXTRACT(data, '$.outer.inner'))`
87 /// - SQLite: `json_extract(data, '$.outer.inner')`
88 /// - SQL Server: `JSON_VALUE(data, '$.outer.inner')`
89 fn json_extract_scalar(&self, column: &str, path: &[String]) -> String;
90
91 /// Next parameter placeholder. Called with the current 1-based index.
92 ///
93 /// - PostgreSQL: `$1`, `$2`, …
94 /// - SQL Server: `@p1`, `@p2`, …
95 /// - MySQL / SQLite: `?`
96 fn placeholder(&self, n: usize) -> String;
97
98 // ── Numeric / boolean casts (have defaults) ────────────────────────────────
99
100 /// Wrap a JSON-extracted scalar expression so it compares numerically.
101 ///
102 /// Default: no cast (MySQL / SQLite coerce implicitly).
103 fn cast_to_numeric<'a>(&self, expr: &'a str) -> Cow<'a, str> {
104 Cow::Borrowed(expr)
105 }
106
107 /// Wrap a JSON-extracted scalar expression so it compares as a boolean.
108 ///
109 /// Default: no cast.
110 fn cast_to_boolean<'a>(&self, expr: &'a str) -> Cow<'a, str> {
111 Cow::Borrowed(expr)
112 }
113
114 /// Wrap a parameter placeholder for numeric comparison.
115 ///
116 /// PostgreSQL uses `({p}::text)::numeric` to avoid wire-protocol type
117 /// mismatch when the driver sends JSON numbers as text. All other dialects
118 /// pass the placeholder through unchanged because their type coercion
119 /// handles it transparently.
120 ///
121 /// Default: no cast (MySQL, SQLite, SQL Server).
122 fn cast_param_numeric<'a>(&self, placeholder: &'a str) -> Cow<'a, str> {
123 Cow::Borrowed(placeholder)
124 }
125
126 // ── LIKE / pattern matching ────────────────────────────────────────────────
127
128 /// SQL fragment for case-sensitive LIKE: `lhs LIKE rhs`.
129 fn like_sql(&self, lhs: &str, rhs: &str) -> String {
130 format!("{lhs} LIKE {rhs}")
131 }
132
133 /// SQL fragment for case-insensitive LIKE.
134 ///
135 /// Default: `LOWER(lhs) LIKE LOWER(rhs)` (MySQL / SQLite compatible).
136 /// PostgreSQL overrides with `lhs ILIKE rhs`.
137 /// SQL Server overrides with `lhs LIKE rhs COLLATE Latin1_General_CI_AI`.
138 fn ilike_sql(&self, lhs: &str, rhs: &str) -> String {
139 format!("LOWER({lhs}) LIKE LOWER({rhs})")
140 }
141
142 /// String concatenation operator / function for building LIKE patterns.
143 ///
144 /// Default: `||` (ANSI SQL — works for PostgreSQL and SQLite).
145 /// MySQL overrides with `CONCAT(…)`.
146 /// SQL Server overrides with `+`.
147 fn concat_sql(&self, parts: &[&str]) -> String {
148 parts.join(" || ")
149 }
150
151 // ── Empty clause sentinels ─────────────────────────────────────────────────
152
153 /// SQL literal for "always false" (used for empty IN clauses, empty OR).
154 ///
155 /// Default: `FALSE`. SQLite and SQL Server use `1=0`.
156 fn always_false(&self) -> &'static str {
157 "FALSE"
158 }
159
160 /// SQL literal for "always true" (used for empty AND).
161 ///
162 /// Default: `TRUE`. SQLite and SQL Server use `1=1`.
163 fn always_true(&self) -> &'static str {
164 "TRUE"
165 }
166
167 // ── Inequality operator ────────────────────────────────────────────────────
168
169 /// SQL inequality operator. Default `!=`. SQL Server uses `<>`.
170 fn neq_operator(&self) -> &'static str {
171 "!="
172 }
173
174 // ── Array length function ──────────────────────────────────────────────────
175
176 /// SQL expression for the length of a JSON array stored in `expr`.
177 fn json_array_length(&self, expr: &str) -> String;
178
179 // ── Array containment (returns Err if not supported) ──────────────────────
180
181 /// SQL for "array contains this element".
182 ///
183 /// Default: returns `Err(UnsupportedOperator)`.
184 ///
185 /// # Errors
186 ///
187 /// Returns [`UnsupportedOperator`] if this dialect does not support array containment.
188 fn array_contains_sql(&self, _lhs: &str, _rhs: &str) -> Result<String, UnsupportedOperator> {
189 Err(UnsupportedOperator {
190 dialect: self.name(),
191 operator: "ArrayContains",
192 })
193 }
194
195 /// SQL for "element is contained by array".
196 ///
197 /// # Errors
198 ///
199 /// Returns [`UnsupportedOperator`] if this dialect does not support array containment.
200 fn array_contained_by_sql(
201 &self,
202 _lhs: &str,
203 _rhs: &str,
204 ) -> Result<String, UnsupportedOperator> {
205 Err(UnsupportedOperator {
206 dialect: self.name(),
207 operator: "ArrayContainedBy",
208 })
209 }
210
211 /// SQL for "arrays overlap".
212 ///
213 /// # Errors
214 ///
215 /// Returns [`UnsupportedOperator`] if this dialect does not support array overlap.
216 fn array_overlaps_sql(&self, _lhs: &str, _rhs: &str) -> Result<String, UnsupportedOperator> {
217 Err(UnsupportedOperator {
218 dialect: self.name(),
219 operator: "ArrayOverlaps",
220 })
221 }
222
223 // ── Full-text search (returns Err if not supported) ────────────────────────
224
225 /// SQL for `to_tsvector(expr) @@ to_tsquery(param)`.
226 ///
227 /// # Errors
228 ///
229 /// Returns [`UnsupportedOperator`] if this dialect does not support full-text search.
230 fn fts_matches_sql(&self, _expr: &str, _param: &str) -> Result<String, UnsupportedOperator> {
231 Err(UnsupportedOperator {
232 dialect: self.name(),
233 operator: "Matches",
234 })
235 }
236
237 /// SQL for plain-text full-text search.
238 ///
239 /// # Errors
240 ///
241 /// Returns [`UnsupportedOperator`] if this dialect does not support plain-text FTS.
242 fn fts_plain_query_sql(
243 &self,
244 _expr: &str,
245 _param: &str,
246 ) -> Result<String, UnsupportedOperator> {
247 Err(UnsupportedOperator {
248 dialect: self.name(),
249 operator: "PlainQuery",
250 })
251 }
252
253 /// SQL for phrase full-text search.
254 ///
255 /// # Errors
256 ///
257 /// Returns [`UnsupportedOperator`] if this dialect does not support phrase FTS.
258 fn fts_phrase_query_sql(
259 &self,
260 _expr: &str,
261 _param: &str,
262 ) -> Result<String, UnsupportedOperator> {
263 Err(UnsupportedOperator {
264 dialect: self.name(),
265 operator: "PhraseQuery",
266 })
267 }
268
269 /// SQL for web-search full-text search.
270 ///
271 /// # Errors
272 ///
273 /// Returns [`UnsupportedOperator`] if this dialect does not support web-search FTS.
274 fn fts_websearch_query_sql(
275 &self,
276 _expr: &str,
277 _param: &str,
278 ) -> Result<String, UnsupportedOperator> {
279 Err(UnsupportedOperator {
280 dialect: self.name(),
281 operator: "WebsearchQuery",
282 })
283 }
284
285 // ── Regex (returns Err if not supported) ───────────────────────────────────
286
287 /// SQL for POSIX-style regex match.
288 ///
289 /// Default: returns `Err(UnsupportedOperator)`.
290 /// PostgreSQL overrides with `~`, `~*`, `!~`, `!~*`.
291 /// MySQL overrides with `REGEXP` / `NOT REGEXP`.
292 ///
293 /// # Errors
294 ///
295 /// Returns [`UnsupportedOperator`] if this dialect does not support regex matching.
296 fn regex_sql(
297 &self,
298 _lhs: &str,
299 _rhs: &str,
300 _case_insensitive: bool,
301 _negate: bool,
302 ) -> Result<String, UnsupportedOperator> {
303 Err(UnsupportedOperator {
304 dialect: self.name(),
305 operator: "Regex",
306 })
307 }
308
309 // ── PostgreSQL-only operators (Vector, Network, LTree) ────────────────────
310 // These methods have default `Err` implementations; only `PostgresDialect`
311 // overrides them. Callers push parameter values before calling these methods
312 // and pass the already-generated placeholder strings.
313
314 /// Generate SQL for a pgvector distance operator.
315 ///
316 /// `pg_op` is one of `<=>`, `<->`, `<+>`, `<~>`, `<#>`.
317 /// `lhs` / `rhs` are the field expression and the placeholder string.
318 ///
319 /// # Errors
320 ///
321 /// Returns [`UnsupportedOperator`] if this dialect does not support vector distance.
322 fn vector_distance_sql(
323 &self,
324 _pg_op: &str,
325 _lhs: &str,
326 _rhs: &str,
327 ) -> Result<String, UnsupportedOperator> {
328 Err(UnsupportedOperator {
329 dialect: self.name(),
330 operator: "VectorDistance",
331 })
332 }
333
334 /// Generate SQL for Jaccard distance (`::text[] <%> ::text[]`).
335 ///
336 /// # Errors
337 ///
338 /// Returns [`UnsupportedOperator`] if this dialect does not support Jaccard distance.
339 fn jaccard_distance_sql(&self, _lhs: &str, _rhs: &str) -> Result<String, UnsupportedOperator> {
340 Err(UnsupportedOperator {
341 dialect: self.name(),
342 operator: "JaccardDistance",
343 })
344 }
345
346 /// Generate SQL for an INET unary check (IsIPv4, IsIPv6, IsPrivate, IsPublic, IsLoopback).
347 ///
348 /// `check_name` identifies the operator (passed to `UnsupportedOperator` on failure).
349 ///
350 /// # Errors
351 ///
352 /// Returns [`UnsupportedOperator`] if this dialect does not support INET checks.
353 fn inet_check_sql(&self, _lhs: &str, _check_name: &str) -> Result<String, UnsupportedOperator> {
354 Err(UnsupportedOperator {
355 dialect: self.name(),
356 operator: "InetCheck",
357 })
358 }
359
360 /// Generate SQL for an INET binary operation (InSubnet, ContainsSubnet, ContainsIP, Overlaps).
361 ///
362 /// `pg_op` is one of `<<`, `>>`, `&&`.
363 ///
364 /// # Errors
365 ///
366 /// Returns [`UnsupportedOperator`] if this dialect does not support INET binary operations.
367 fn inet_binary_sql(
368 &self,
369 _pg_op: &str,
370 _lhs: &str,
371 _rhs: &str,
372 ) -> Result<String, UnsupportedOperator> {
373 Err(UnsupportedOperator {
374 dialect: self.name(),
375 operator: "InetBinaryOp",
376 })
377 }
378
379 /// Generate SQL for an LTree binary operator.
380 ///
381 /// `pg_op` is one of `@>`, `<@`, `~`, `@`.
382 /// `rhs_type` is the cast type for `rhs` (e.g., `"ltree"`, `"lquery"`, `"ltxtquery"`).
383 ///
384 /// # Errors
385 ///
386 /// Returns [`UnsupportedOperator`] if this dialect does not support LTree operations.
387 fn ltree_binary_sql(
388 &self,
389 _pg_op: &str,
390 _lhs: &str,
391 _rhs: &str,
392 _rhs_type: &str,
393 ) -> Result<String, UnsupportedOperator> {
394 Err(UnsupportedOperator {
395 dialect: self.name(),
396 operator: "LTreeBinaryOp",
397 })
398 }
399
400 /// Generate SQL for `ltree ? ARRAY[...]` (MatchesAnyLquery).
401 ///
402 /// `placeholders` contains pre-formatted placeholder strings
403 /// (e.g., `["$1::lquery", "$2::lquery"]`).
404 ///
405 /// # Errors
406 ///
407 /// Returns [`UnsupportedOperator`] if this dialect does not support LTree lquery arrays.
408 fn ltree_any_lquery_sql(
409 &self,
410 _lhs: &str,
411 _placeholders: &[String],
412 ) -> Result<String, UnsupportedOperator> {
413 Err(UnsupportedOperator {
414 dialect: self.name(),
415 operator: "MatchesAnyLquery",
416 })
417 }
418
419 /// Generate SQL for `nlevel(ltree) OP param` (depth comparison operators).
420 ///
421 /// # Errors
422 ///
423 /// Returns [`UnsupportedOperator`] if this dialect does not support LTree depth comparisons.
424 fn ltree_depth_sql(
425 &self,
426 _op: &str,
427 _lhs: &str,
428 _rhs: &str,
429 ) -> Result<String, UnsupportedOperator> {
430 Err(UnsupportedOperator {
431 dialect: self.name(),
432 operator: "LTreeDepth",
433 })
434 }
435
436 /// Generate SQL for `ltree = lca(ARRAY[...])` (Lca operator).
437 ///
438 /// `placeholders` contains pre-formatted placeholder strings
439 /// (e.g., `["$1::ltree", "$2::ltree"]`).
440 ///
441 /// # Errors
442 ///
443 /// Returns [`UnsupportedOperator`] if this dialect does not support LTree LCA.
444 fn ltree_lca_sql(
445 &self,
446 _lhs: &str,
447 _placeholders: &[String],
448 ) -> Result<String, UnsupportedOperator> {
449 Err(UnsupportedOperator {
450 dialect: self.name(),
451 operator: "Lca",
452 })
453 }
454
455 // ── Row-view DDL helpers (for gRPC transport) ──────────────────────────────
456
457 /// Generate a SQL expression that extracts a scalar field from a JSON column
458 /// and casts it to the given [`RowViewColumnType`].
459 ///
460 /// `json_column` is the unquoted column name (e.g., `"data"`).
461 /// `field_name` is the JSON key to extract.
462 /// `col_type` selects the target SQL type.
463 ///
464 /// Default: panics — each dialect must override.
465 fn row_view_column_expr(
466 &self,
467 json_column: &str,
468 field_name: &str,
469 col_type: &RowViewColumnType,
470 ) -> String {
471 let _ = (json_column, field_name, col_type);
472 panic!("{} dialect has not implemented row_view_column_expr", self.name())
473 }
474
475 /// Generate the full DDL statement(s) to create a row-shaped view.
476 ///
477 /// `view_name` is the unquoted view name (e.g., `"vr_user"`).
478 /// `source_table` is the unquoted source table (e.g., `"tb_user"`).
479 /// `columns` are `(alias, expression)` pairs from [`Self::row_view_column_expr`].
480 ///
481 /// Default: `CREATE OR REPLACE VIEW "view" AS SELECT ... FROM "table"`.
482 fn create_row_view_ddl(
483 &self,
484 view_name: &str,
485 source_table: &str,
486 columns: &[(String, String)],
487 ) -> String {
488 let quoted_view = self.quote_identifier(view_name);
489 let quoted_table = self.quote_identifier(source_table);
490 let col_list: Vec<String> = columns
491 .iter()
492 .map(|(alias, expr)| format!("{expr} AS {}", self.quote_identifier(alias)))
493 .collect();
494 format!(
495 "CREATE OR REPLACE VIEW {quoted_view} AS\nSELECT\n {}\nFROM {quoted_table};",
496 col_list.join(",\n ")
497 )
498 }
499
500 // ── Extended operators (Email, VIN, IBAN, …) ───────────────────────────────
501
502 /// Generate SQL for an extended rich-type operator.
503 ///
504 /// Default: returns a validation error (operator not supported).
505 /// Each dialect overrides this to provide dialect-specific SQL functions
506 /// (e.g. `SPLIT_PART` for PostgreSQL, `SUBSTRING_INDEX` for MySQL).
507 ///
508 /// # Errors
509 ///
510 /// Returns `FraiseQLError::Validation` if the operator is not supported
511 /// by this dialect or the parameters are invalid.
512 fn generate_extended_sql(
513 &self,
514 operator: &crate::filters::ExtendedOperator,
515 _field_sql: &str,
516 _params: &mut Vec<serde_json::Value>,
517 ) -> fraiseql_error::Result<String> {
518 Err(fraiseql_error::FraiseQLError::validation(format!(
519 "Extended operator {operator} is not supported by the {} dialect",
520 self.name()
521 )))
522 }
523}