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/// Returns the identifier quoted with double quotes if needed.
58/// Handles dotted identifiers (e.g., `table.column`) by quoting each part.
59pub fn escape_identifier(name: &str) -> String {
60    // Handle dotted identifiers (e.g., lm.phone_number -> "lm"."phone_number")
61    if name.contains('.') {
62        return name
63            .split('.')
64            .map(escape_single_identifier)
65            .collect::<Vec<_>>()
66            .join(".");
67    }
68    escape_single_identifier(name)
69}
70
71/// Escape a single identifier part (no dots).
72fn escape_single_identifier(name: &str) -> String {
73    let lower = name.to_lowercase();
74    let needs_escaping = RESERVED_WORDS.contains(&lower.as_str())
75        || name.chars().any(|c| !c.is_alphanumeric() && c != '_')
76        || name.chars().next().map(|c| c.is_numeric()).unwrap_or(false);
77
78    if needs_escaping {
79        format!("\"{}\"", name.replace('"', "\"\""))
80    } else {
81        name.to_string()
82    }
83}
84
85/// Trait for dialect-specific SQL generation.
86pub trait SqlGenerator {
87    /// Quote an identifier (table or column name).
88    fn quote_identifier(&self, name: &str) -> String;
89    /// Generate the parameter placeholder (e.g., $1, ?, @p1) for a given index.
90    fn placeholder(&self, index: usize) -> String;
91    /// Get the fuzzy matching operator (ILIKE vs LIKE).
92    fn fuzzy_operator(&self) -> &str;
93    /// Get the boolean literal (true/false vs 1/0).
94    fn bool_literal(&self, val: bool) -> String;
95    /// Generate string concatenation expression (e.g. 'a' || 'b' vs CONCAT('a', 'b')).
96    fn string_concat(&self, parts: &[&str]) -> String;
97    /// Generate LIMIT/OFFSET clause.
98    fn limit_offset(&self, limit: Option<usize>, offset: Option<usize>) -> String;
99    /// Generate JSON access syntax.
100    /// path components are the keys to traverse.
101    /// Default implementation returns "col"."key1"."key2" (Standard SQL composite).
102    fn json_access(&self, col: &str, path: &[&str]) -> String {
103        let mut parts = vec![self.quote_identifier(col)];
104        for key in path {
105            parts.push(self.quote_identifier(key));
106        }
107        parts.join(".")
108    }
109    /// Generate JSON/Array contains expression.
110    /// Default implementation returns Postgres-compatible `col @> value`.
111    fn json_contains(&self, col: &str, value: &str) -> String {
112        format!("{} @> {}", col, value)
113    }
114    /// Generate JSON key exists expression.
115    /// Default implementation returns Postgres-compatible `col ? 'key'`.
116    fn json_key_exists(&self, col: &str, key: &str) -> String {
117        format!("{} ? {}", col, key)
118    }
119
120    /// JSON_EXISTS - check if path exists in JSON (Postgres 17+, SQL/JSON standard)
121    fn json_exists(&self, col: &str, path: &str) -> String {
122        format!("JSON_EXISTS({}, '{}')", col, path)
123    }
124
125    /// JSON_QUERY - extract JSON object/array at path (Postgres 17+, SQL/JSON standard)
126    fn json_query(&self, col: &str, path: &str) -> String {
127        format!("JSON_QUERY({}, '{}')", col, path)
128    }
129
130    /// JSON_VALUE - extract scalar value at path (Postgres 17+, SQL/JSON standard)
131    fn json_value(&self, col: &str, path: &str) -> String {
132        format!("JSON_VALUE({}, '{}')", col, path)
133    }
134}