Skip to main content

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    /// Wrap a parameter placeholder for a native-column equality condition.
127    ///
128    /// `native_type` is the PostgreSQL canonical type name stored in
129    /// `native_columns` (e.g. `"uuid"`, `"int4"`, `"timestamp"`).
130    ///
131    /// **Only PostgreSQL needs to override this.**  tokio-postgres uses a binary
132    /// wire protocol: when the query contains `$1::uuid`, the server resolves `$1`
133    /// as OID 2950 and expects 16-byte binary UUID encoding, but `QueryParam::Text`
134    /// sends UTF-8 bytes, causing "incorrect binary data format".  The two-step
135    /// cast `$1::text::uuid` forces the server to resolve `$1` as `text` first.
136    ///
137    /// sqlx (MySQL, SQLite) and tiberius (SQL Server) send string parameters as
138    /// text by default regardless of column type, so no cast is needed — the
139    /// database coerces the text value at comparison time.
140    ///
141    /// Default: return the placeholder unchanged (correct for MySQL, SQLite,
142    /// SQL Server).
143    fn cast_native_param(&self, placeholder: &str, _native_type: &str) -> String {
144        placeholder.to_string()
145    }
146
147    // ── LIKE / pattern matching ────────────────────────────────────────────────
148
149    /// SQL fragment for case-sensitive LIKE: `lhs LIKE rhs`.
150    fn like_sql(&self, lhs: &str, rhs: &str) -> String {
151        format!("{lhs} LIKE {rhs}")
152    }
153
154    /// SQL fragment for case-insensitive LIKE.
155    ///
156    /// Default: `LOWER(lhs) LIKE LOWER(rhs)` (MySQL / SQLite compatible).
157    /// PostgreSQL overrides with `lhs ILIKE rhs`.
158    /// SQL Server overrides with `lhs LIKE rhs COLLATE Latin1_General_CI_AI`.
159    fn ilike_sql(&self, lhs: &str, rhs: &str) -> String {
160        format!("LOWER({lhs}) LIKE LOWER({rhs})")
161    }
162
163    /// String concatenation operator / function for building LIKE patterns.
164    ///
165    /// Default: `||` (ANSI SQL — works for PostgreSQL and SQLite).
166    /// MySQL overrides with `CONCAT(…)`.
167    /// SQL Server overrides with `+`.
168    fn concat_sql(&self, parts: &[&str]) -> String {
169        parts.join(" || ")
170    }
171
172    // ── Empty clause sentinels ─────────────────────────────────────────────────
173
174    /// SQL literal for "always false" (used for empty IN clauses, empty OR).
175    ///
176    /// Default: `FALSE`. SQLite and SQL Server use `1=0`.
177    fn always_false(&self) -> &'static str {
178        "FALSE"
179    }
180
181    /// SQL literal for "always true" (used for empty AND).
182    ///
183    /// Default: `TRUE`. SQLite and SQL Server use `1=1`.
184    fn always_true(&self) -> &'static str {
185        "TRUE"
186    }
187
188    // ── Inequality operator ────────────────────────────────────────────────────
189
190    /// SQL inequality operator.  Default `!=`.  SQL Server uses `<>`.
191    fn neq_operator(&self) -> &'static str {
192        "!="
193    }
194
195    // ── Array length function ──────────────────────────────────────────────────
196
197    /// SQL expression for the length of a JSON array stored in `expr`.
198    fn json_array_length(&self, expr: &str) -> String;
199
200    // ── Array containment (returns Err if not supported) ──────────────────────
201
202    /// SQL for "array contains this element".
203    ///
204    /// Default: returns `Err(UnsupportedOperator)`.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`UnsupportedOperator`] if this dialect does not support array containment.
209    fn array_contains_sql(&self, _lhs: &str, _rhs: &str) -> Result<String, UnsupportedOperator> {
210        Err(UnsupportedOperator {
211            dialect:  self.name(),
212            operator: "ArrayContains",
213        })
214    }
215
216    /// SQL for "element is contained by array".
217    ///
218    /// # Errors
219    ///
220    /// Returns [`UnsupportedOperator`] if this dialect does not support array containment.
221    fn array_contained_by_sql(
222        &self,
223        _lhs: &str,
224        _rhs: &str,
225    ) -> Result<String, UnsupportedOperator> {
226        Err(UnsupportedOperator {
227            dialect:  self.name(),
228            operator: "ArrayContainedBy",
229        })
230    }
231
232    /// SQL for "arrays overlap".
233    ///
234    /// # Errors
235    ///
236    /// Returns [`UnsupportedOperator`] if this dialect does not support array overlap.
237    fn array_overlaps_sql(&self, _lhs: &str, _rhs: &str) -> Result<String, UnsupportedOperator> {
238        Err(UnsupportedOperator {
239            dialect:  self.name(),
240            operator: "ArrayOverlaps",
241        })
242    }
243
244    // ── Full-text search (returns Err if not supported) ────────────────────────
245
246    /// SQL for `to_tsvector(expr) @@ to_tsquery(param)`.
247    ///
248    /// # Errors
249    ///
250    /// Returns [`UnsupportedOperator`] if this dialect does not support full-text search.
251    fn fts_matches_sql(&self, _expr: &str, _param: &str) -> Result<String, UnsupportedOperator> {
252        Err(UnsupportedOperator {
253            dialect:  self.name(),
254            operator: "Matches",
255        })
256    }
257
258    /// SQL for plain-text full-text search.
259    ///
260    /// # Errors
261    ///
262    /// Returns [`UnsupportedOperator`] if this dialect does not support plain-text FTS.
263    fn fts_plain_query_sql(
264        &self,
265        _expr: &str,
266        _param: &str,
267    ) -> Result<String, UnsupportedOperator> {
268        Err(UnsupportedOperator {
269            dialect:  self.name(),
270            operator: "PlainQuery",
271        })
272    }
273
274    /// SQL for phrase full-text search.
275    ///
276    /// # Errors
277    ///
278    /// Returns [`UnsupportedOperator`] if this dialect does not support phrase FTS.
279    fn fts_phrase_query_sql(
280        &self,
281        _expr: &str,
282        _param: &str,
283    ) -> Result<String, UnsupportedOperator> {
284        Err(UnsupportedOperator {
285            dialect:  self.name(),
286            operator: "PhraseQuery",
287        })
288    }
289
290    /// SQL for web-search full-text search.
291    ///
292    /// # Errors
293    ///
294    /// Returns [`UnsupportedOperator`] if this dialect does not support web-search FTS.
295    fn fts_websearch_query_sql(
296        &self,
297        _expr: &str,
298        _param: &str,
299    ) -> Result<String, UnsupportedOperator> {
300        Err(UnsupportedOperator {
301            dialect:  self.name(),
302            operator: "WebsearchQuery",
303        })
304    }
305
306    // ── Regex (returns Err if not supported) ───────────────────────────────────
307
308    /// SQL for POSIX-style regex match.
309    ///
310    /// Default: returns `Err(UnsupportedOperator)`.
311    /// PostgreSQL overrides with `~`, `~*`, `!~`, `!~*`.
312    /// MySQL overrides with `REGEXP` / `NOT REGEXP`.
313    ///
314    /// # Errors
315    ///
316    /// Returns [`UnsupportedOperator`] if this dialect does not support regex matching.
317    fn regex_sql(
318        &self,
319        _lhs: &str,
320        _rhs: &str,
321        _case_insensitive: bool,
322        _negate: bool,
323    ) -> Result<String, UnsupportedOperator> {
324        Err(UnsupportedOperator {
325            dialect:  self.name(),
326            operator: "Regex",
327        })
328    }
329
330    // ── PostgreSQL-only operators (Vector, Network, LTree) ────────────────────
331    // These methods have default `Err` implementations; only `PostgresDialect`
332    // overrides them.  Callers push parameter values before calling these methods
333    // and pass the already-generated placeholder strings.
334
335    /// Generate SQL for a pgvector distance operator.
336    ///
337    /// `pg_op` is one of `<=>`, `<->`, `<+>`, `<~>`, `<#>`.
338    /// `lhs` / `rhs` are the field expression and the placeholder string.
339    ///
340    /// # Errors
341    ///
342    /// Returns [`UnsupportedOperator`] if this dialect does not support vector distance.
343    fn vector_distance_sql(
344        &self,
345        _pg_op: &str,
346        _lhs: &str,
347        _rhs: &str,
348    ) -> Result<String, UnsupportedOperator> {
349        Err(UnsupportedOperator {
350            dialect:  self.name(),
351            operator: "VectorDistance",
352        })
353    }
354
355    /// Generate SQL for Jaccard distance (`::text[] <%> ::text[]`).
356    ///
357    /// # Errors
358    ///
359    /// Returns [`UnsupportedOperator`] if this dialect does not support Jaccard distance.
360    fn jaccard_distance_sql(&self, _lhs: &str, _rhs: &str) -> Result<String, UnsupportedOperator> {
361        Err(UnsupportedOperator {
362            dialect:  self.name(),
363            operator: "JaccardDistance",
364        })
365    }
366
367    /// Generate SQL for an INET unary check (IsIPv4, IsIPv6, IsPrivate, IsPublic, IsLoopback).
368    ///
369    /// `check_name` identifies the operator (passed to `UnsupportedOperator` on failure).
370    ///
371    /// # Errors
372    ///
373    /// Returns [`UnsupportedOperator`] if this dialect does not support INET checks.
374    fn inet_check_sql(&self, _lhs: &str, _check_name: &str) -> Result<String, UnsupportedOperator> {
375        Err(UnsupportedOperator {
376            dialect:  self.name(),
377            operator: "InetCheck",
378        })
379    }
380
381    /// Generate SQL for an INET binary operation (InSubnet, ContainsSubnet, ContainsIP, Overlaps).
382    ///
383    /// `pg_op` is one of `<<`, `>>`, `&&`.
384    ///
385    /// # Errors
386    ///
387    /// Returns [`UnsupportedOperator`] if this dialect does not support INET binary operations.
388    fn inet_binary_sql(
389        &self,
390        _pg_op: &str,
391        _lhs: &str,
392        _rhs: &str,
393    ) -> Result<String, UnsupportedOperator> {
394        Err(UnsupportedOperator {
395            dialect:  self.name(),
396            operator: "InetBinaryOp",
397        })
398    }
399
400    /// Generate SQL for an LTree binary operator.
401    ///
402    /// `pg_op` is one of `@>`, `<@`, `~`, `@`.
403    /// `rhs_type` is the cast type for `rhs` (e.g., `"ltree"`, `"lquery"`, `"ltxtquery"`).
404    ///
405    /// # Errors
406    ///
407    /// Returns [`UnsupportedOperator`] if this dialect does not support LTree operations.
408    fn ltree_binary_sql(
409        &self,
410        _pg_op: &str,
411        _lhs: &str,
412        _rhs: &str,
413        _rhs_type: &str,
414    ) -> Result<String, UnsupportedOperator> {
415        Err(UnsupportedOperator {
416            dialect:  self.name(),
417            operator: "LTreeBinaryOp",
418        })
419    }
420
421    /// Generate SQL for `ltree ? ARRAY[...]` (MatchesAnyLquery).
422    ///
423    /// `placeholders` contains pre-formatted placeholder strings
424    /// (e.g., `["$1::lquery", "$2::lquery"]`).
425    ///
426    /// # Errors
427    ///
428    /// Returns [`UnsupportedOperator`] if this dialect does not support LTree lquery arrays.
429    fn ltree_any_lquery_sql(
430        &self,
431        _lhs: &str,
432        _placeholders: &[String],
433    ) -> Result<String, UnsupportedOperator> {
434        Err(UnsupportedOperator {
435            dialect:  self.name(),
436            operator: "MatchesAnyLquery",
437        })
438    }
439
440    /// Generate SQL for `nlevel(ltree) OP param` (depth comparison operators).
441    ///
442    /// # Errors
443    ///
444    /// Returns [`UnsupportedOperator`] if this dialect does not support LTree depth comparisons.
445    fn ltree_depth_sql(
446        &self,
447        _op: &str,
448        _lhs: &str,
449        _rhs: &str,
450    ) -> Result<String, UnsupportedOperator> {
451        Err(UnsupportedOperator {
452            dialect:  self.name(),
453            operator: "LTreeDepth",
454        })
455    }
456
457    /// Generate SQL for `ltree = lca(ARRAY[...])` (Lca operator).
458    ///
459    /// `placeholders` contains pre-formatted placeholder strings
460    /// (e.g., `["$1::ltree", "$2::ltree"]`).
461    ///
462    /// # Errors
463    ///
464    /// Returns [`UnsupportedOperator`] if this dialect does not support LTree LCA.
465    fn ltree_lca_sql(
466        &self,
467        _lhs: &str,
468        _placeholders: &[String],
469    ) -> Result<String, UnsupportedOperator> {
470        Err(UnsupportedOperator {
471            dialect:  self.name(),
472            operator: "Lca",
473        })
474    }
475
476    // ── Row-view DDL helpers (for gRPC transport) ──────────────────────────────
477
478    /// Generate a SQL expression that extracts a scalar field from a JSON column
479    /// and casts it to the given [`RowViewColumnType`].
480    ///
481    /// `json_column` is the unquoted column name (e.g., `"data"`).
482    /// `field_name` is the JSON key to extract.
483    /// `col_type` selects the target SQL type.
484    ///
485    /// Default: panics — each dialect must override.
486    fn row_view_column_expr(
487        &self,
488        json_column: &str,
489        field_name: &str,
490        col_type: &RowViewColumnType,
491    ) -> String {
492        let _ = (json_column, field_name, col_type);
493        panic!("{} dialect has not implemented row_view_column_expr", self.name())
494    }
495
496    /// Generate the full DDL statement(s) to create a row-shaped view.
497    ///
498    /// `view_name` is the unquoted view name (e.g., `"vr_user"`).
499    /// `source_table` is the unquoted source table (e.g., `"tb_user"`).
500    /// `columns` are `(alias, expression)` pairs from [`Self::row_view_column_expr`].
501    ///
502    /// Default: `CREATE OR REPLACE VIEW "view" AS SELECT ... FROM "table"`.
503    fn create_row_view_ddl(
504        &self,
505        view_name: &str,
506        source_table: &str,
507        columns: &[(String, String)],
508    ) -> String {
509        let quoted_view = self.quote_identifier(view_name);
510        let quoted_table = self.quote_identifier(source_table);
511        let col_list: Vec<String> = columns
512            .iter()
513            .map(|(alias, expr)| format!("{expr} AS {}", self.quote_identifier(alias)))
514            .collect();
515        format!(
516            "CREATE OR REPLACE VIEW {quoted_view} AS\nSELECT\n  {}\nFROM {quoted_table};",
517            col_list.join(",\n  ")
518        )
519    }
520
521    // ── Extended operators (Email, VIN, IBAN, …) ───────────────────────────────
522
523    /// Generate SQL for an extended rich-type operator.
524    ///
525    /// Default: returns a validation error (operator not supported).
526    /// Each dialect overrides this to provide dialect-specific SQL functions
527    /// (e.g. `SPLIT_PART` for PostgreSQL, `SUBSTRING_INDEX` for MySQL).
528    ///
529    /// # Errors
530    ///
531    /// Returns `FraiseQLError::Validation` if the operator is not supported
532    /// by this dialect or the parameters are invalid.
533    fn generate_extended_sql(
534        &self,
535        operator: &crate::filters::ExtendedOperator,
536        _field_sql: &str,
537        _params: &mut Vec<serde_json::Value>,
538    ) -> fraiseql_error::Result<String> {
539        Err(fraiseql_error::FraiseQLError::validation(format!(
540            "Extended operator {operator} is not supported by the {} dialect",
541            self.name()
542        )))
543    }
544}