1use crate::ast::*;
6
7const RESERVED_WORDS: &[&str] = &[
9 "order", "group", "user", "table", "select", "from", "where", "join",
10 "left", "right", "inner", "outer", "on", "and", "or", "not", "null",
11 "true", "false", "limit", "offset", "as", "in", "is", "like", "between",
12 "having", "union", "all", "distinct", "case", "when", "then", "else", "end",
13 "create", "alter", "drop", "insert", "update", "delete", "index", "key",
14 "primary", "foreign", "references", "default", "constraint", "check",
15];
16
17pub fn escape_identifier(name: &str) -> String {
20 let lower = name.to_lowercase();
21 let needs_escaping = RESERVED_WORDS.contains(&lower.as_str())
22 || name.chars().any(|c| !c.is_alphanumeric() && c != '_')
23 || name.chars().next().map(|c| c.is_numeric()).unwrap_or(false);
24
25 if needs_escaping {
26 format!("\"{}\"", name.replace('"', "\"\""))
27 } else {
28 name.to_string()
29 }
30}
31
32pub trait ToSql {
34 fn to_sql(&self) -> String;
36}
37
38impl ToSql for QailCmd {
39 fn to_sql(&self) -> String {
40 match self.action {
41 Action::Get => self.to_select_sql(),
42 Action::Set => self.to_update_sql(),
43 Action::Del => self.to_delete_sql(),
44 Action::Add => self.to_insert_sql(),
45 Action::Gen => format!("-- gen::{} (generates Rust struct, not SQL)", self.table),
46 Action::Make => self.to_create_table_sql(),
47 Action::Mod => self.to_alter_table_sql(),
48 Action::Over => self.to_window_sql(),
49 Action::With => self.to_cte_sql(),
50 }
51 }
52}
53
54impl QailCmd {
55 fn to_select_sql(&self) -> String {
57 let mut sql = if self.distinct {
58 String::from("SELECT DISTINCT ")
59 } else {
60 String::from("SELECT ")
61 };
62
63 if self.columns.is_empty() {
65 sql.push('*');
66 } else {
67 let cols: Vec<String> = self.columns.iter().map(|c| c.to_string()).collect();
68 sql.push_str(&cols.join(", "));
69 }
70
71 sql.push_str(" FROM ");
73 sql.push_str(&self.table);
74
75 for join in &self.joins {
77 let kind = match join.kind {
78 JoinKind::Inner => "INNER",
79 JoinKind::Left => "LEFT",
80 JoinKind::Right => "RIGHT",
81 };
82 let source_singular = self.table.trim_end_matches('s');
85 sql.push_str(&format!(
86 " {} JOIN {} ON {}.{}_id = {}.id",
87 kind, join.table, join.table, source_singular, self.table
88 ));
89 }
90
91 let has_aggregates = self.columns.iter().any(|c| matches!(c, Column::Aggregate { .. }));
93 let mut non_aggregated_cols = Vec::new();
94 if has_aggregates {
95 for col in &self.columns {
96 if let Column::Named(name) = col {
97 non_aggregated_cols.push(name.clone());
98 }
99 }
100 }
101
102 let mut where_groups: Vec<String> = Vec::new();
104 let mut order_by: Option<String> = None;
105 let mut limit: Option<usize> = None;
106 let mut offset: Option<usize> = None;
107
108 for cage in &self.cages {
109 match &cage.kind {
110 CageKind::Filter => {
111 if !cage.conditions.is_empty() {
112 let joiner = match cage.logical_op {
113 LogicalOp::And => " AND ",
114 LogicalOp::Or => " OR ",
115 };
116 let conditions: Vec<String> = cage.conditions.iter().map(|c| c.to_sql()).collect();
117 let group = conditions.join(joiner);
118 if cage.logical_op == LogicalOp::Or && cage.conditions.len() > 1 {
120 where_groups.push(format!("({})", group));
121 } else {
122 where_groups.push(group);
123 }
124 }
125 }
126 CageKind::Sort(order) => {
127 if let Some(cond) = cage.conditions.first() {
128 let dir = match order {
129 SortOrder::Asc => "ASC",
130 SortOrder::Desc => "DESC",
131 };
132 order_by = Some(format!("{} {}", cond.column, dir));
133 }
134 }
135 CageKind::Limit(n) => {
136 limit = Some(*n);
137 }
138 CageKind::Offset(n) => {
139 offset = Some(*n);
140 }
141 CageKind::Payload => {
142 }
144 }
145 }
146
147 if !where_groups.is_empty() {
149 sql.push_str(" WHERE ");
150 sql.push_str(&where_groups.join(" AND "));
151 }
152
153 if !non_aggregated_cols.is_empty() {
155 sql.push_str(" GROUP BY ");
156 sql.push_str(&non_aggregated_cols.join(", "));
157 }
158
159 if let Some(order) = order_by {
161 sql.push_str(" ORDER BY ");
162 sql.push_str(&order);
163 }
164
165 if let Some(n) = limit {
167 sql.push_str(&format!(" LIMIT {}", n));
168 }
169
170 if let Some(n) = offset {
172 sql.push_str(&format!(" OFFSET {}", n));
173 }
174
175 sql
176 }
177
178 fn to_update_sql(&self) -> String {
180 let mut sql = String::from("UPDATE ");
181 sql.push_str(&self.table);
182
183 let mut set_clauses: Vec<String> = Vec::new();
185 let mut where_clauses: Vec<String> = Vec::new();
186 let mut is_first_filter = true;
187
188 for cage in &self.cages {
189 if let CageKind::Filter = cage.kind {
190 if is_first_filter {
191 for cond in &cage.conditions {
193 set_clauses.push(format!("{} = {}", cond.column, cond.value));
194 }
195 is_first_filter = false;
196 } else {
197 for cond in &cage.conditions {
199 where_clauses.push(cond.to_sql());
200 }
201 }
202 }
203 }
204
205 if !set_clauses.is_empty() {
207 sql.push_str(" SET ");
208 sql.push_str(&set_clauses.join(", "));
209 }
210
211 if !where_clauses.is_empty() {
213 sql.push_str(" WHERE ");
214 sql.push_str(&where_clauses.join(" AND "));
215 }
216
217 sql
218 }
219
220 fn to_delete_sql(&self) -> String {
222 let mut sql = String::from("DELETE FROM ");
223 sql.push_str(&self.table);
224
225 let mut where_clauses: Vec<String> = Vec::new();
227
228 for cage in &self.cages {
229 if let CageKind::Filter = cage.kind {
230 for cond in &cage.conditions {
231 where_clauses.push(cond.to_sql());
232 }
233 }
234 }
235
236 if !where_clauses.is_empty() {
237 sql.push_str(" WHERE ");
238 sql.push_str(&where_clauses.join(" AND "));
239 }
240
241 sql
242 }
243
244 fn to_insert_sql(&self) -> String {
246 let mut sql = String::from("INSERT INTO ");
247 sql.push_str(&self.table);
248
249 let cols: Vec<String> = self.columns.iter().map(|c| c.to_string()).collect();
251
252 if !cols.is_empty() {
253 sql.push_str(" (");
254 sql.push_str(&cols.join(", "));
255 sql.push(')');
256 }
257
258 if let Some(cage) = self.cages.first() {
260 let values: Vec<String> = cage.conditions.iter().map(|c| c.value.to_string()).collect();
261
262 if !values.is_empty() {
263 sql.push_str(" VALUES (");
264 sql.push_str(&values.join(", "));
265 sql.push(')');
266 }
267 }
268
269 if !self.columns.is_empty() {
271 let cols: Vec<String> = self.columns.iter().map(|c| c.to_string()).collect();
272 sql.push_str(" RETURNING ");
273 sql.push_str(&cols.join(", "));
274 } else {
275 sql.push_str(" RETURNING *");
277 }
278
279 sql
280 }
281
282 fn to_create_table_sql(&self) -> String {
284 let mut sql = String::new();
285 sql.push_str("CREATE TABLE ");
286 sql.push_str(&self.table);
287 sql.push_str(" (\n");
288
289 let mut defs = Vec::new();
290 for col in &self.columns {
291 if let Column::Def {
292 name,
293 data_type,
294 constraints,
295 } = col
296 {
297 let sql_type = map_type(data_type);
298 let mut line = format!(" {} {}", name, sql_type);
299
300 let is_nullable = constraints.contains(&Constraint::Nullable);
302 if !is_nullable {
303 line.push_str(" NOT NULL");
304 }
305
306 if constraints.contains(&Constraint::PrimaryKey) {
307 line.push_str(" PRIMARY KEY");
308 }
309 if constraints.contains(&Constraint::Unique) {
310 line.push_str(" UNIQUE");
311 }
312 defs.push(line);
313 }
314 }
315 sql.push_str(&defs.join(",\n"));
316 sql.push_str("\n)");
317 sql
318 }
319
320 fn to_alter_table_sql(&self) -> String {
322 let mut stmts = Vec::new();
323 for col in &self.columns {
324 match col {
325 Column::Mod { kind, col } => {
326 match kind {
327 ModKind::Add => {
328 if let Column::Def { name, data_type, constraints } = col.as_ref() {
329 let sql_type = map_type(data_type);
330 let mut line = format!("ALTER TABLE {} ADD COLUMN {} {}", self.table, name, sql_type);
331
332 let is_nullable = constraints.contains(&Constraint::Nullable);
333 if !is_nullable {
334 line.push_str(" NOT NULL");
335 }
336
337 if constraints.contains(&Constraint::Unique) {
338 line.push_str(" UNIQUE");
339 }
340 stmts.push(line);
341 }
342 }
343 ModKind::Drop => {
344 if let Column::Named(name) = col.as_ref() {
345 stmts.push(format!("ALTER TABLE {} DROP COLUMN {}", self.table, name));
346 }
347 }
348 }
349 }
350 _ => {}
351 }
352 }
353 stmts.join(";\n")
354 }
355
356 fn to_window_sql(&self) -> String {
358 let mut sql = String::from("SELECT ");
360
361 let cols: Vec<String> = self.columns.iter().map(|c| {
362 match c {
363 Column::Window { name, func, params, partition, order } => {
364 let params_str = if params.is_empty() {
365 String::new()
366 } else {
367 params.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")
368 };
369
370 let mut over_clause = String::from("OVER (");
371 if !partition.is_empty() {
372 over_clause.push_str("PARTITION BY ");
373 over_clause.push_str(&partition.join(", "));
374 if !order.is_empty() {
375 over_clause.push(' ');
376 }
377 }
378 if !order.is_empty() {
379 over_clause.push_str("ORDER BY ");
380 let order_parts: Vec<String> = order.iter().map(|cage| {
381 match &cage.kind {
382 CageKind::Sort(SortOrder::Asc) => {
383 if let Some(cond) = cage.conditions.first() {
384 format!("{} ASC", cond.column)
385 } else {
386 String::new()
387 }
388 }
389 CageKind::Sort(SortOrder::Desc) => {
390 if let Some(cond) = cage.conditions.first() {
391 format!("{} DESC", cond.column)
392 } else {
393 String::new()
394 }
395 }
396 _ => String::new(),
397 }
398 }).filter(|s| !s.is_empty()).collect();
399 over_clause.push_str(&order_parts.join(", "));
400 }
401 over_clause.push(')');
402
403 format!("{}({}) {} AS {}", func, params_str, over_clause, name)
404 }
405 _ => c.to_string(),
406 }
407 }).collect();
408
409 sql.push_str(&cols.join(", "));
410 sql.push_str(" FROM ");
411 sql.push_str(&self.table);
412
413 let where_clauses: Vec<String> = self.cages.iter()
415 .filter(|c| matches!(c.kind, CageKind::Filter))
416 .flat_map(|c| c.conditions.iter().map(|cond| cond.to_sql()))
417 .collect();
418
419 if !where_clauses.is_empty() {
420 sql.push_str(" WHERE ");
421 sql.push_str(&where_clauses.join(" AND "));
422 }
423
424 sql
425 }
426
427 fn to_cte_sql(&self) -> String {
429 let mut sql = String::from("WITH ");
432 sql.push_str(&self.table);
433 sql.push_str(" AS (");
434
435 sql.push_str("SELECT ");
437 if self.columns.is_empty() {
438 sql.push('*');
439 } else {
440 let cols: Vec<String> = self.columns.iter().map(|c| c.to_string()).collect();
441 sql.push_str(&cols.join(", "));
442 }
443 sql.push_str(" FROM ");
444 sql.push_str(&self.table);
445
446 let where_clauses: Vec<String> = self.cages.iter()
448 .filter(|c| matches!(c.kind, CageKind::Filter))
449 .flat_map(|c| c.conditions.iter().map(|cond| cond.to_sql()))
450 .collect();
451 if !where_clauses.is_empty() {
452 sql.push_str(" WHERE ");
453 sql.push_str(&where_clauses.join(" AND "));
454 }
455
456 for cage in &self.cages {
457 if let CageKind::Limit(n) = cage.kind {
458 sql.push_str(&format!(" LIMIT {}", n));
459 }
460 }
461
462 sql.push_str(") SELECT * FROM ");
463 sql.push_str(&self.table);
464
465 sql
466 }
467}
468
469fn map_type(t: &str) -> &str {
470 match t {
471 "str" | "text" | "string" => "VARCHAR(255)",
472 "int" | "i32" => "INT",
473 "bigint" | "i64" => "BIGINT",
474 "uuid" => "UUID",
475 "bool" | "boolean" => "BOOLEAN",
476 "dec" | "decimal" => "DECIMAL",
477 "float" | "f64" => "DOUBLE PRECISION",
478 "serial" => "SERIAL",
479 "timestamp" | "time" => "TIMESTAMP",
480 "json" | "jsonb" => "JSONB",
481 _ => t,
482 }
483}
484
485impl Condition {
486 fn to_sql(&self) -> String {
488 if self.is_array_unnest {
490 let inner_condition = match self.op {
491 Operator::Eq => format!("_el = {}", self.value),
492 Operator::Ne => format!("_el != {}", self.value),
493 Operator::Gt => format!("_el > {}", self.value),
494 Operator::Gte => format!("_el >= {}", self.value),
495 Operator::Lt => format!("_el < {}", self.value),
496 Operator::Lte => format!("_el <= {}", self.value),
497 Operator::Fuzzy => {
498 let val = match &self.value {
499 Value::String(s) => format!("'%{}%'", s),
500 Value::Param(n) => format!("'%' || ${} || '%'", n),
501 v => format!("'%{}%'", v),
502 };
503 format!("_el ILIKE {}", val)
504 }
505 _ => format!("_el = {}", self.value),
506 };
507 return format!(
508 "EXISTS (SELECT 1 FROM unnest({}) _el WHERE {})",
509 self.column, inner_condition
510 );
511 }
512
513 match self.op {
515 Operator::Eq => format!("{} = {}", self.column, self.value),
516 Operator::Ne => format!("{} != {}", self.column, self.value),
517 Operator::Gt => format!("{} > {}", self.column, self.value),
518 Operator::Gte => format!("{} >= {}", self.column, self.value),
519 Operator::Lt => format!("{} < {}", self.column, self.value),
520 Operator::Lte => format!("{} <= {}", self.column, self.value),
521 Operator::Fuzzy => {
522 let val = match &self.value {
524 Value::String(s) => format!("'%{}%'", s),
525 Value::Param(n) => format!("'%' || ${} || '%'", n),
526 v => format!("'%{}%'", v),
527 };
528 format!("{} ILIKE {}", self.column, val)
529 }
530 Operator::In => format!("{} = ANY({})", self.column, self.value),
531 Operator::NotIn => format!("{} != ALL({})", self.column, self.value),
532 Operator::IsNull => format!("{} IS NULL", self.column),
533 Operator::IsNotNull => format!("{} IS NOT NULL", self.column),
534 }
535 }
536}
537
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use crate::parser::parse;
543
544 #[test]
545 fn test_simple_select() {
546 let cmd = parse("get::users:'_").unwrap();
547 assert_eq!(cmd.to_sql(), "SELECT * FROM users");
548 }
549
550 #[test]
551 fn test_select_columns() {
552 let cmd = parse("get::users:'id'email'role").unwrap();
553 assert_eq!(cmd.to_sql(), "SELECT id, email, role FROM users");
554 }
555
556 #[test]
557 fn test_select_with_where() {
558 let cmd = parse("get::users:'_[active=true]").unwrap();
559 assert_eq!(cmd.to_sql(), "SELECT * FROM users WHERE active = true");
560 }
561
562 #[test]
563 fn test_select_with_limit() {
564 let cmd = parse("get::users:'_[lim=10]").unwrap();
565 assert_eq!(cmd.to_sql(), "SELECT * FROM users LIMIT 10");
566 }
567
568 #[test]
569 fn test_select_with_order() {
570 let cmd = parse("get::users:'_[^!created_at]").unwrap();
571 assert_eq!(cmd.to_sql(), "SELECT * FROM users ORDER BY created_at DESC");
572 }
573
574 #[test]
575 fn test_select_complex() {
576 let cmd = parse("get::users:'id'email[active=true][^!created_at][lim=10]").unwrap();
577 assert_eq!(
578 cmd.to_sql(),
579 "SELECT id, email FROM users WHERE active = true ORDER BY created_at DESC LIMIT 10"
580 );
581 }
582
583 #[test]
584 fn test_update() {
585 let cmd = parse("set::users:[verified=true][id=$1]").unwrap();
586 assert_eq!(cmd.to_sql(), "UPDATE users SET verified = true WHERE id = $1");
587 }
588
589 #[test]
590 fn test_delete() {
591 let cmd = parse("del::sessions:[expired_at<now]").unwrap();
592 assert_eq!(cmd.to_sql(), "DELETE FROM sessions WHERE expired_at < now()");
594 }
595
596 #[test]
597 fn test_fuzzy_match() {
598 let cmd = parse("get::users:'_[name~$1]").unwrap();
599 assert_eq!(cmd.to_sql(), "SELECT * FROM users WHERE name ILIKE '%' || $1 || '%'");
600 }
601
602 #[test]
603 fn test_or_conditions() {
604 let cmd = parse("get::users:'_[role=admin|role=mod]").unwrap();
605 assert_eq!(cmd.to_sql(), "SELECT * FROM users WHERE (role = 'admin' OR role = 'mod')");
606 }
607
608 #[test]
609 fn test_mixed_and_or() {
610 let cmd = parse("get::users:'_[active=true][role=admin|role=mod]").unwrap();
611 assert_eq!(cmd.to_sql(), "SELECT * FROM users WHERE active = true AND (role = 'admin' OR role = 'mod')");
612 }
613
614 #[test]
615 fn test_array_unnest() {
616 let cmd = parse("get::posts:'_[tags[*]~$1]").unwrap();
617 assert_eq!(cmd.to_sql(), "SELECT * FROM posts WHERE EXISTS (SELECT 1 FROM unnest(tags) _el WHERE _el ILIKE '%' || $1 || '%')");
618 }
619
620 #[test]
621 fn test_complex_array_search() {
622 let cmd = parse("get::kb:'_[active=true][keywords[*]~$1|question~$1]").unwrap();
623 assert_eq!(
624 cmd.to_sql(),
625 "SELECT * FROM kb WHERE active = true AND (EXISTS (SELECT 1 FROM unnest(keywords) _el WHERE _el ILIKE '%' || $1 || '%') OR question ILIKE '%' || $1 || '%')"
626 );
627 }
628
629 #[test]
630 fn test_offset_pagination() {
631 let cmd = parse("get::users:'_[lim=10][off=20]").unwrap();
632 assert_eq!(cmd.to_sql(), "SELECT * FROM users LIMIT 10 OFFSET 20");
633 }
634
635 #[test]
636 fn test_insert_returning() {
637 let cmd = parse("add::users:'id'email[name=John]").unwrap();
638 let sql = cmd.to_sql();
639 assert!(sql.contains("RETURNING"));
640 }
641
642 #[test]
643 fn test_escape_reserved_words() {
644 assert_eq!(escape_identifier("users"), "users");
645 assert_eq!(escape_identifier("order"), "\"order\"");
646 assert_eq!(escape_identifier("group"), "\"group\"");
647 assert_eq!(escape_identifier("user"), "\"user\"");
648 }
649
650 #[test]
651 fn test_left_join() {
652 let cmd = parse("get::users<-profiles:'id'name").unwrap();
653 let sql = cmd.to_sql();
654 assert!(sql.contains("LEFT JOIN"));
655 }
656
657 #[test]
658 fn test_right_join() {
659 let cmd = parse("get::users->>profiles:'id'name").unwrap();
660 let sql = cmd.to_sql();
661 assert!(sql.contains("RIGHT JOIN"));
662 }
663
664 #[test]
665 fn test_distinct() {
666 let cmd = parse("get!::users:'role").unwrap();
667 assert!(cmd.distinct);
668 assert_eq!(cmd.to_sql(), "SELECT DISTINCT role FROM users");
669 }
670}
671