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('$') {
209 field.to_string()
210 } else if field.starts_with("meta.") {
211 format!("$.{}", field)
213 } else {
214 format!("$.data.{}", field)
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_simple_field_query() {
226 let query = Query::field_eq("name", "Alice");
227 let sql = SqlTranslator::translate(&query, "payload");
228 assert_eq!(sql.clause, "JSON_UNQUOTE(JSON_EXTRACT(payload, '$.data.name')) = ?");
229 assert_eq!(sql.params, vec![SqlParam::Text("Alice".to_string())]);
230 }
231
232 #[test]
233 fn test_inline_simple_field() {
234 let query = Query::field_eq("name", "Alice");
235 let sql = SqlTranslator::translate_inline(&query, "payload");
236 assert_eq!(sql, "JSON_UNQUOTE(JSON_EXTRACT(payload, '$.data.name')) = 'Alice'");
237 }
238
239 #[test]
240 fn test_numeric_range() {
241 let query = Query::numeric_range("age", Some(25.0), Some(40.0));
242 let sql = SqlTranslator::translate(&query, "payload");
243 assert_eq!(sql.clause, "JSON_EXTRACT(payload, '$.data.age') BETWEEN ? AND ?");
244 assert_eq!(
245 sql.params,
246 vec![SqlParam::Numeric(25.0), SqlParam::Numeric(40.0)]
247 );
248 }
249
250 #[test]
251 fn test_numeric_range_unbounded_min() {
252 let query = Query::numeric_range("age", None, Some(40.0));
253 let sql = SqlTranslator::translate(&query, "payload");
254 assert_eq!(sql.clause, "JSON_EXTRACT(payload, '$.data.age') <= ?");
255 assert_eq!(sql.params, vec![SqlParam::Numeric(40.0)]);
256 }
257
258 #[test]
259 fn test_numeric_range_unbounded_max() {
260 let query = Query::numeric_range("score", Some(100.0), None);
261 let sql = SqlTranslator::translate(&query, "payload");
262 assert_eq!(sql.clause, "JSON_EXTRACT(payload, '$.data.score') >= ?");
263 assert_eq!(sql.params, vec![SqlParam::Numeric(100.0)]);
264 }
265
266 #[test]
267 fn test_tag_query() {
268 let query = Query::tags("tags", vec!["rust".to_string(), "database".to_string()]);
269 let sql = SqlTranslator::translate(&query, "payload");
270 assert_eq!(
271 sql.clause,
272 "(JSON_CONTAINS(payload->'$.data.tags', ?) OR JSON_CONTAINS(payload->'$.data.tags', ?))"
273 );
274 assert_eq!(
275 sql.params,
276 vec![
277 SqlParam::Text("\"rust\"".to_string()),
278 SqlParam::Text("\"database\"".to_string())
279 ]
280 );
281 }
282
283 #[test]
284 fn test_and_query() {
285 let query =
286 Query::field_eq("name", "Alice").and(Query::numeric_range("age", Some(25.0), Some(40.0)));
287 let sql = SqlTranslator::translate(&query, "payload");
288 assert_eq!(
289 sql.clause,
290 "(JSON_UNQUOTE(JSON_EXTRACT(payload, '$.data.name')) = ? AND JSON_EXTRACT(payload, '$.data.age') BETWEEN ? AND ?)"
291 );
292 assert_eq!(
293 sql.params,
294 vec![
295 SqlParam::Text("Alice".to_string()),
296 SqlParam::Numeric(25.0),
297 SqlParam::Numeric(40.0)
298 ]
299 );
300 }
301
302 #[test]
303 fn test_or_query() {
304 let query =
305 Query::field_eq("status", "active").or(Query::field_eq("status", "pending"));
306 let sql = SqlTranslator::translate(&query, "payload");
307 assert_eq!(
308 sql.clause,
309 "(JSON_UNQUOTE(JSON_EXTRACT(payload, '$.data.status')) = ? OR JSON_UNQUOTE(JSON_EXTRACT(payload, '$.data.status')) = ?)"
310 );
311 }
312
313 #[test]
314 fn test_not_query() {
315 let query = Query::field_eq("deleted", "true").negate();
316 let sql = SqlTranslator::translate(&query, "payload");
317 assert_eq!(
318 sql.clause,
319 "NOT (JSON_UNQUOTE(JSON_EXTRACT(payload, '$.data.deleted')) = ?)"
320 );
321 }
322
323 #[test]
324 fn test_contains_query() {
325 let query = Query::text_search("description", "database");
326 let sql = SqlTranslator::translate(&query, "payload");
327 assert_eq!(
328 sql.clause,
329 "JSON_UNQUOTE(JSON_EXTRACT(payload, '$.data.description')) LIKE ?"
330 );
331 assert_eq!(sql.params, vec![SqlParam::Text("%database%".to_string())]);
332 }
333
334 #[test]
335 fn test_prefix_query() {
336 let query = Query::prefix("email", "admin");
337 let sql = SqlTranslator::translate(&query, "payload");
338 assert_eq!(
339 sql.clause,
340 "JSON_UNQUOTE(JSON_EXTRACT(payload, '$.data.email')) LIKE ?"
341 );
342 assert_eq!(sql.params, vec![SqlParam::Text("admin%".to_string())]);
343 }
344
345 #[test]
346 fn test_nested_field() {
347 let query = Query::field_eq("user.profile.name", "Alice");
348 let sql = SqlTranslator::translate(&query, "payload");
349 assert_eq!(
350 sql.clause,
351 "JSON_UNQUOTE(JSON_EXTRACT(payload, '$.data.user.profile.name')) = ?"
352 );
353 }
354
355 #[test]
356 fn test_complex_query() {
357 let alice_query =
358 Query::field_eq("name", "Alice").and(Query::numeric_range("age", Some(25.0), Some(40.0)));
359
360 let bob_query = Query::field_eq("name", "Bob")
361 .and(Query::tags("tags", vec!["rust".to_string()]));
362
363 let query = alice_query.or(bob_query);
364 let sql = SqlTranslator::translate(&query, "payload");
365
366 assert!(sql.clause.starts_with("(("));
368 assert!(sql.clause.contains(" AND "));
369 assert!(sql.clause.contains(" OR "));
370 assert!(sql.clause.contains("$.data.name"));
371 }
372}