1pub 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
56pub 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
69fn 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
83pub fn escape_sql_string_literal(value: &str) -> String {
89 value.replace('\'', "''")
90}
91
92fn is_sql_placeholder(value: &str) -> bool {
93 value == "?"
94 || value
95 .strip_prefix('$')
96 .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()))
97 || value
98 .strip_prefix("@p")
99 .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()))
100 || value.strip_prefix(':').is_some_and(|rest| {
101 !rest.is_empty() && rest.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
102 })
103}
104
105fn sql_json_path_argument(path: &str) -> String {
106 if is_sql_placeholder(path) {
107 path.to_string()
108 } else {
109 format!("'{}'", escape_sql_string_literal(path))
110 }
111}
112
113pub trait SqlGenerator {
115 fn quote_identifier(&self, name: &str) -> String;
117 fn placeholder(&self, index: usize) -> String;
119 fn fuzzy_operator(&self) -> &str;
121 fn bool_literal(&self, val: bool) -> String;
123 fn string_concat(&self, parts: &[&str]) -> String;
125 fn limit_offset(&self, limit: Option<usize>, offset: Option<usize>) -> String;
127 fn json_access(&self, col: &str, path: &[&str]) -> String {
131 let mut parts = vec![self.quote_identifier(col)];
132 for key in path {
133 parts.push(self.quote_identifier(key));
134 }
135 parts.join(".")
136 }
137 fn json_contains(&self, col: &str, value: &str) -> String {
140 format!("{} @> {}", col, value)
141 }
142 fn json_key_exists(&self, col: &str, key: &str) -> String {
145 format!("{} ? {}", col, key)
146 }
147
148 fn json_exists(&self, col: &str, path: &str) -> String {
150 format!("JSON_EXISTS({}, {})", col, sql_json_path_argument(path))
151 }
152
153 fn json_query(&self, col: &str, path: &str) -> String {
155 format!("JSON_QUERY({}, {})", col, sql_json_path_argument(path))
156 }
157
158 fn json_value(&self, col: &str, path: &str) -> String {
160 format!("JSON_VALUE({}, {})", col, sql_json_path_argument(path))
161 }
162
163 fn in_array(&self, col: &str, value: &str) -> String {
166 format!("{} = ANY({})", col, value)
167 }
168
169 fn not_in_array(&self, col: &str, value: &str) -> String {
172 format!("{} != ALL({})", col, value)
173 }
174}