qail_core/transpiler/
traits.rs

1//! Transpiler traits and utilities.
2
3/// SQL reserved words that must be quoted when used as identifiers.
4pub const RESERVED_WORDS: &[&str] = &[
5    "order",
6    "group",
7    "user",
8    "table",
9    "select",
10    "from",
11    "where",
12    "join",
13    "left",
14    "right",
15    "inner",
16    "outer",
17    "on",
18    "and",
19    "or",
20    "not",
21    "null",
22    "true",
23    "false",
24    "limit",
25    "offset",
26    "as",
27    "in",
28    "is",
29    "like",
30    "between",
31    "having",
32    "union",
33    "all",
34    "distinct",
35    "case",
36    "when",
37    "then",
38    "else",
39    "end",
40    "create",
41    "alter",
42    "drop",
43    "insert",
44    "update",
45    "delete",
46    "index",
47    "key",
48    "primary",
49    "foreign",
50    "references",
51    "default",
52    "constraint",
53    "check",
54];
55
56/// Escape an identifier if it's a reserved word or contains special chars.
57/// Handles dotted identifiers (e.g., `table.column`) by quoting each part.
58pub fn escape_identifier(name: &str) -> String {
59    if name.contains('.') {
60        return name
61            .split('.')
62            .map(escape_single_identifier)
63            .collect::<Vec<_>>()
64            .join(".");
65    }
66    escape_single_identifier(name)
67}
68
69/// Escape a single identifier part (no dots).
70fn escape_single_identifier(name: &str) -> String {
71    let lower = name.to_lowercase();
72    let needs_escaping = RESERVED_WORDS.contains(&lower.as_str())
73        || name.chars().any(|c| !c.is_alphanumeric() && c != '_')
74        || name.chars().next().map(|c| c.is_numeric()).unwrap_or(false);
75
76    if needs_escaping {
77        format!("\"{}\"", name.replace('"', "\"\""))
78    } else {
79        name.to_string()
80    }
81}
82
83/// Trait for dialect-specific SQL generation.
84pub trait SqlGenerator {
85    /// Quote an identifier (table or column name).
86    fn quote_identifier(&self, name: &str) -> String;
87    /// Generate the parameter placeholder (e.g., $1, ?, @p1) for a given index.
88    fn placeholder(&self, index: usize) -> String;
89    /// Get the fuzzy matching operator (ILIKE vs LIKE).
90    fn fuzzy_operator(&self) -> &str;
91    /// Get the boolean literal (true/false vs 1/0).
92    fn bool_literal(&self, val: bool) -> String;
93    /// Generate string concatenation expression (e.g. 'a' || 'b' vs CONCAT('a', 'b')).
94    fn string_concat(&self, parts: &[&str]) -> String;
95    fn limit_offset(&self, limit: Option<usize>, offset: Option<usize>) -> String;
96    /// Generate JSON access syntax.
97    /// path components are the keys to traverse.
98    /// Default implementation returns "col"."key1"."key2" (Standard SQL composite).
99    fn json_access(&self, col: &str, path: &[&str]) -> String {
100        let mut parts = vec![self.quote_identifier(col)];
101        for key in path {
102            parts.push(self.quote_identifier(key));
103        }
104        parts.join(".")
105    }
106    /// Generate JSON/Array contains expression.
107    /// Default implementation returns Postgres-compatible `col @> value`.
108    fn json_contains(&self, col: &str, value: &str) -> String {
109        format!("{} @> {}", col, value)
110    }
111    /// Generate JSON key exists expression.
112    /// Default implementation returns Postgres-compatible `col ? 'key'`.
113    fn json_key_exists(&self, col: &str, key: &str) -> String {
114        format!("{} ? {}", col, key)
115    }
116
117    /// JSON_EXISTS - check if path exists in JSON (Postgres 17+, SQL/JSON standard)
118    fn json_exists(&self, col: &str, path: &str) -> String {
119        format!("JSON_EXISTS({}, '{}')", col, path)
120    }
121
122    /// JSON_QUERY - extract JSON object/array at path (Postgres 17+, SQL/JSON standard)
123    fn json_query(&self, col: &str, path: &str) -> String {
124        format!("JSON_QUERY({}, '{}')", col, path)
125    }
126
127    /// JSON_VALUE - extract scalar value at path (Postgres 17+, SQL/JSON standard)
128    fn json_value(&self, col: &str, path: &str) -> String {
129        format!("JSON_VALUE({}, '{}')", col, path)
130    }
131
132    /// Generate IN array check (col IN value)
133    /// Default: Postgres-style `col = ANY(value)` for array params
134    fn in_array(&self, col: &str, value: &str) -> String {
135        format!("{} = ANY({})", col, value)
136    }
137
138    /// Generate NOT IN array check (col NOT IN value)
139    /// Default: Postgres-style `col != ALL(value)` for array params
140    fn not_in_array(&self, col: &str, value: &str) -> String {
141        format!("{} != ALL({})", col, value)
142    }
143}