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", "group", "user", "table", "select", "from", "where", "join",
6    "left", "right", "inner", "outer", "on", "and", "or", "not", "null",
7    "true", "false", "limit", "offset", "as", "in", "is", "like", "between",
8    "having", "union", "all", "distinct", "case", "when", "then", "else", "end",
9    "create", "alter", "drop", "insert", "update", "delete", "index", "key",
10    "primary", "foreign", "references", "default", "constraint", "check",
11];
12
13/// Escape an identifier if it's a reserved word or contains special chars.
14/// Returns the identifier quoted with double quotes if needed.
15/// Handles dotted identifiers (e.g., `table.column`) by quoting each part.
16pub fn escape_identifier(name: &str) -> String {
17    // Handle dotted identifiers (e.g., lm.phone_number -> "lm"."phone_number")
18    if name.contains('.') {
19        return name
20            .split('.')
21            .map(|part| escape_single_identifier(part))
22            .collect::<Vec<_>>()
23            .join(".");
24    }
25    escape_single_identifier(name)
26}
27
28/// Escape a single identifier part (no dots).
29fn escape_single_identifier(name: &str) -> String {
30    let lower = name.to_lowercase();
31    let needs_escaping = RESERVED_WORDS.contains(&lower.as_str())
32        || name.chars().any(|c| !c.is_alphanumeric() && c != '_')
33        || name.chars().next().map(|c| c.is_numeric()).unwrap_or(false);
34    
35    if needs_escaping {
36        format!("\"{}\"", name.replace('"', "\"\""))
37    } else {
38        name.to_string()
39    }
40}
41
42/// Trait for dialect-specific SQL generation.
43pub trait SqlGenerator {
44    /// Quote an identifier (table or column name).
45    fn quote_identifier(&self, name: &str) -> String;
46    /// Generate the parameter placeholder (e.g., $1, ?, @p1) for a given index.
47    fn placeholder(&self, index: usize) -> String;
48    /// Get the fuzzy matching operator (ILIKE vs LIKE).
49    fn fuzzy_operator(&self) -> &str;
50    /// Get the boolean literal (true/false vs 1/0).
51    fn bool_literal(&self, val: bool) -> String;
52    /// Generate string concatenation expression (e.g. 'a' || 'b' vs CONCAT('a', 'b')).
53    fn string_concat(&self, parts: &[&str]) -> String;
54    /// Generate LIMIT/OFFSET clause.
55    fn limit_offset(&self, limit: Option<usize>, offset: Option<usize>) -> String;
56    /// Generate JSON access syntax.
57    /// path components are the keys to traverse.
58    /// Default implementation returns "col"."key1"."key2" (Standard SQL composite).
59    fn json_access(&self, col: &str, path: &[&str]) -> String {
60        let mut parts = vec![self.quote_identifier(col)];
61        for key in path {
62            parts.push(self.quote_identifier(key));
63        }
64        parts.join(".")
65    }
66    /// Generate JSON/Array contains expression.
67    /// Default implementation returns Postgres-compatible `col @> value`.
68    fn json_contains(&self, col: &str, value: &str) -> String {
69        format!("{} @> {}", col, value)
70    }
71    /// Generate JSON key exists expression.
72    /// Default implementation returns Postgres-compatible `col ? 'key'`.
73    fn json_key_exists(&self, col: &str, key: &str) -> String {
74        format!("{} ? {}", col, key)
75    }
76    
77    /// JSON_EXISTS - check if path exists in JSON (Postgres 17+, SQL/JSON standard)
78    fn json_exists(&self, col: &str, path: &str) -> String {
79        format!("JSON_EXISTS({}, '{}')", col, path)
80    }
81    
82    /// JSON_QUERY - extract JSON object/array at path (Postgres 17+, SQL/JSON standard)
83    fn json_query(&self, col: &str, path: &str) -> String {
84        format!("JSON_QUERY({}, '{}')", col, path)
85    }
86    
87    /// JSON_VALUE - extract scalar value at path (Postgres 17+, SQL/JSON standard)
88    fn json_value(&self, col: &str, path: &str) -> String {
89        format!("JSON_VALUE({}, '{}')", col, path)
90    }
91}