1use super::query_builder::{FieldOperator, FieldQuery, Query, QueryNode, QueryValue};
20
21pub struct SqlTranslator;
23
24#[derive(Debug, Clone)]
26pub struct SqlQuery {
27 pub clause: String,
29 pub params: Vec<SqlParam>,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub enum SqlParam {
36 Text(String),
37 Numeric(f64),
38 Boolean(bool),
39}
40
41impl SqlTranslator {
42 pub fn translate(query: &Query, json_column: &str) -> SqlQuery {
46 let mut params = Vec::new();
47 let clause = Self::translate_node(&query.root, json_column, &mut params);
48 SqlQuery { clause, params }
49 }
50
51 pub fn translate_inline(query: &Query, json_column: &str) -> String {
55 let mut params = Vec::new();
56 let clause = Self::translate_node(&query.root, json_column, &mut params);
57
58 let mut result = clause;
60 for param in params {
61 let value = match param {
62 SqlParam::Text(s) => format!("'{}'", s.replace('\'', "''")),
63 SqlParam::Numeric(n) => n.to_string(),
64 SqlParam::Boolean(b) => if b { "TRUE" } else { "FALSE" }.to_string(),
65 };
66 result = result.replacen('?', &value, 1);
67 }
68 result
69 }
70
71 fn translate_node(node: &QueryNode, json_col: &str, params: &mut Vec<SqlParam>) -> String {
72 match node {
73 QueryNode::Field(field_query) => Self::translate_field(field_query, json_col, params),
74 QueryNode::And(nodes) => {
75 let parts: Vec<String> = nodes
77 .iter()
78 .filter(|n| !matches!(n, QueryNode::Vector(_)))
79 .map(|n| Self::translate_node(n, json_col, params))
80 .collect();
81 if parts.is_empty() {
82 "1=1".to_string()
84 } else if parts.len() == 1 {
85 parts[0].clone()
86 } else {
87 format!("({})", parts.join(" AND "))
88 }
89 }
90 QueryNode::Or(nodes) => {
91 let parts: Vec<String> = nodes
92 .iter()
93 .filter(|n| !matches!(n, QueryNode::Vector(_)))
94 .map(|n| Self::translate_node(n, json_col, params))
95 .collect();
96 if parts.is_empty() {
97 "1=1".to_string()
98 } else if parts.len() == 1 {
99 parts[0].clone()
100 } else {
101 format!("({})", parts.join(" OR "))
102 }
103 }
104 QueryNode::Not(inner) => {
105 format!("NOT ({})", Self::translate_node(inner, json_col, params))
106 }
107 QueryNode::Vector(_) => {
108 "1=0 /* vector search not supported in SQL */".to_string()
112 }
113 }
114 }
115
116 fn translate_field(
117 field: &FieldQuery,
118 json_col: &str,
119 params: &mut Vec<SqlParam>,
120 ) -> String {
121 let json_path = Self::json_path(&field.field);
122
123 match (&field.operator, &field.value) {
124 (FieldOperator::Equals, QueryValue::Text(text)) => {
125 params.push(SqlParam::Text(text.clone()));
126 format!("JSON_UNQUOTE(JSON_EXTRACT({}, '{}')) = ?", json_col, json_path)
127 }
128 (FieldOperator::Equals, QueryValue::Numeric(num)) => {
129 params.push(SqlParam::Numeric(*num));
130 format!("JSON_EXTRACT({}, '{}') = ?", json_col, json_path)
131 }
132 (FieldOperator::Equals, QueryValue::Boolean(b)) => {
133 params.push(SqlParam::Boolean(*b));
134 format!("JSON_EXTRACT({}, '{}') = ?", json_col, json_path)
135 }
136 (FieldOperator::Contains, QueryValue::Text(text)) => {
137 params.push(SqlParam::Text(format!("%{}%", text)));
138 format!(
139 "JSON_UNQUOTE(JSON_EXTRACT({}, '{}')) LIKE ?",
140 json_col, json_path
141 )
142 }
143 (FieldOperator::Range, QueryValue::NumericRange { min, max }) => {
144 match (min, max) {
145 (Some(min_val), Some(max_val)) => {
146 params.push(SqlParam::Numeric(*min_val));
147 params.push(SqlParam::Numeric(*max_val));
148 format!(
149 "JSON_EXTRACT({}, '{}') BETWEEN ? AND ?",
150 json_col, json_path
151 )
152 }
153 (Some(min_val), None) => {
154 params.push(SqlParam::Numeric(*min_val));
155 format!("JSON_EXTRACT({}, '{}') >= ?", json_col, json_path)
156 }
157 (None, Some(max_val)) => {
158 params.push(SqlParam::Numeric(*max_val));
159 format!("JSON_EXTRACT({}, '{}') <= ?", json_col, json_path)
160 }
161 (None, None) => "1=1".to_string(), }
163 }
164 (FieldOperator::In, QueryValue::Tags(tags)) => {
165 let conditions: Vec<String> = tags
168 .iter()
169 .map(|tag| {
170 params.push(SqlParam::Text(format!("\"{}\"", tag)));
171 format!("JSON_CONTAINS({}->'{}', ?)", json_col, json_path)
172 })
173 .collect();
174
175 if conditions.len() == 1 {
176 conditions[0].clone()
177 } else {
178 format!("({})", conditions.join(" OR "))
179 }
180 }
181 (FieldOperator::Prefix, QueryValue::Text(text)) => {
182 params.push(SqlParam::Text(format!("{}%", text)));
183 format!(
184 "JSON_UNQUOTE(JSON_EXTRACT({}, '{}')) LIKE ?",
185 json_col, json_path
186 )
187 }
188 (FieldOperator::Fuzzy, QueryValue::Text(text)) => {
189 params.push(SqlParam::Text(format!("%{}%", text)));
192 format!(
193 "JSON_UNQUOTE(JSON_EXTRACT({}, '{}')) LIKE ?",
194 json_col, json_path
195 )
196 }
197 _ => {
198 "1=0".to_string()
200 }
201 }
202 }
203
204 fn json_path(field: &str) -> String {
205 if field.starts_with('$') {
208 field.to_string()
209 } else {
210 format!("$.{}", field)
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_simple_field_query() {
221 let query = Query::field_eq("name", "Alice");
222 let sql = SqlTranslator::translate(&query, "data");
223 assert_eq!(sql.clause, "JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) = ?");
224 assert_eq!(sql.params, vec![SqlParam::Text("Alice".to_string())]);
225 }
226
227 #[test]
228 fn test_inline_simple_field() {
229 let query = Query::field_eq("name", "Alice");
230 let sql = SqlTranslator::translate_inline(&query, "data");
231 assert_eq!(sql, "JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) = 'Alice'");
232 }
233
234 #[test]
235 fn test_numeric_range() {
236 let query = Query::numeric_range("age", Some(25.0), Some(40.0));
237 let sql = SqlTranslator::translate(&query, "data");
238 assert_eq!(sql.clause, "JSON_EXTRACT(data, '$.age') BETWEEN ? AND ?");
239 assert_eq!(
240 sql.params,
241 vec![SqlParam::Numeric(25.0), SqlParam::Numeric(40.0)]
242 );
243 }
244
245 #[test]
246 fn test_numeric_range_unbounded_min() {
247 let query = Query::numeric_range("age", None, Some(40.0));
248 let sql = SqlTranslator::translate(&query, "data");
249 assert_eq!(sql.clause, "JSON_EXTRACT(data, '$.age') <= ?");
250 assert_eq!(sql.params, vec![SqlParam::Numeric(40.0)]);
251 }
252
253 #[test]
254 fn test_numeric_range_unbounded_max() {
255 let query = Query::numeric_range("score", Some(100.0), None);
256 let sql = SqlTranslator::translate(&query, "data");
257 assert_eq!(sql.clause, "JSON_EXTRACT(data, '$.score') >= ?");
258 assert_eq!(sql.params, vec![SqlParam::Numeric(100.0)]);
259 }
260
261 #[test]
262 fn test_tag_query() {
263 let query = Query::tags("tags", vec!["rust".to_string(), "database".to_string()]);
264 let sql = SqlTranslator::translate(&query, "data");
265 assert_eq!(
266 sql.clause,
267 "(JSON_CONTAINS(data->'$.tags', ?) OR JSON_CONTAINS(data->'$.tags', ?))"
268 );
269 assert_eq!(
270 sql.params,
271 vec![
272 SqlParam::Text("\"rust\"".to_string()),
273 SqlParam::Text("\"database\"".to_string())
274 ]
275 );
276 }
277
278 #[test]
279 fn test_and_query() {
280 let query =
281 Query::field_eq("name", "Alice").and(Query::numeric_range("age", Some(25.0), Some(40.0)));
282 let sql = SqlTranslator::translate(&query, "data");
283 assert_eq!(
284 sql.clause,
285 "(JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) = ? AND JSON_EXTRACT(data, '$.age') BETWEEN ? AND ?)"
286 );
287 assert_eq!(
288 sql.params,
289 vec![
290 SqlParam::Text("Alice".to_string()),
291 SqlParam::Numeric(25.0),
292 SqlParam::Numeric(40.0)
293 ]
294 );
295 }
296
297 #[test]
298 fn test_or_query() {
299 let query =
300 Query::field_eq("status", "active").or(Query::field_eq("status", "pending"));
301 let sql = SqlTranslator::translate(&query, "data");
302 assert_eq!(
303 sql.clause,
304 "(JSON_UNQUOTE(JSON_EXTRACT(data, '$.status')) = ? OR JSON_UNQUOTE(JSON_EXTRACT(data, '$.status')) = ?)"
305 );
306 }
307
308 #[test]
309 fn test_not_query() {
310 let query = Query::field_eq("deleted", "true").negate();
311 let sql = SqlTranslator::translate(&query, "data");
312 assert_eq!(
313 sql.clause,
314 "NOT (JSON_UNQUOTE(JSON_EXTRACT(data, '$.deleted')) = ?)"
315 );
316 }
317
318 #[test]
319 fn test_contains_query() {
320 let query = Query::text_search("description", "database");
321 let sql = SqlTranslator::translate(&query, "data");
322 assert_eq!(
323 sql.clause,
324 "JSON_UNQUOTE(JSON_EXTRACT(data, '$.description')) LIKE ?"
325 );
326 assert_eq!(sql.params, vec![SqlParam::Text("%database%".to_string())]);
327 }
328
329 #[test]
330 fn test_prefix_query() {
331 let query = Query::prefix("email", "admin");
332 let sql = SqlTranslator::translate(&query, "data");
333 assert_eq!(
334 sql.clause,
335 "JSON_UNQUOTE(JSON_EXTRACT(data, '$.email')) LIKE ?"
336 );
337 assert_eq!(sql.params, vec![SqlParam::Text("admin%".to_string())]);
338 }
339
340 #[test]
341 fn test_nested_field() {
342 let query = Query::field_eq("user.profile.name", "Alice");
343 let sql = SqlTranslator::translate(&query, "data");
344 assert_eq!(
345 sql.clause,
346 "JSON_UNQUOTE(JSON_EXTRACT(data, '$.user.profile.name')) = ?"
347 );
348 }
349
350 #[test]
351 fn test_complex_query() {
352 let alice_query =
353 Query::field_eq("name", "Alice").and(Query::numeric_range("age", Some(25.0), Some(40.0)));
354
355 let bob_query = Query::field_eq("name", "Bob")
356 .and(Query::tags("tags", vec!["rust".to_string()]));
357
358 let query = alice_query.or(bob_query);
359 let sql = SqlTranslator::translate(&query, "data");
360
361 assert!(sql.clause.starts_with("(("));
363 assert!(sql.clause.contains(" AND "));
364 assert!(sql.clause.contains(" OR "));
365 }
366}