1use crate::ast::{
14 BinOp, CompareOp, Expr, FieldRef, Filter, InsertQuery, Projection, QueryExpr, QueueCommand,
15 TableQuery, UpdateQuery, UpdateTarget,
16};
17use reddb_types::types::Value;
18
19pub fn render(expr: &QueryExpr) -> String {
23 match expr {
24 QueryExpr::Table(tq) => render_table(tq),
25 QueryExpr::Insert(iq) => render_insert(iq),
26 QueryExpr::Update(uq) => render_update(uq),
27 QueryExpr::QueueCommand(qc) => render_queue_command(qc),
28 _ => String::new(),
29 }
30}
31
32fn render_table(tq: &TableQuery) -> String {
33 let cols = if tq.columns.is_empty() {
34 "*".to_string()
35 } else {
36 tq.columns
37 .iter()
38 .map(render_projection)
39 .collect::<Vec<_>>()
40 .join(", ")
41 };
42 let mut sql = format!("SELECT {} FROM {}", cols, tq.table);
43 if let Some(filter) = &tq.filter {
44 sql.push_str(" WHERE ");
45 sql.push_str(&render_filter(filter));
46 }
47 sql
48}
49
50fn render_insert(iq: &InsertQuery) -> String {
51 let cols = iq.columns.join(", ");
52 let rows: Vec<String> = iq
53 .values
54 .iter()
55 .map(|row| {
56 let vals = row
57 .iter()
58 .map(render_value_sql)
59 .collect::<Vec<_>>()
60 .join(", ");
61 format!("({})", vals)
62 })
63 .collect();
64 format!(
65 "INSERT INTO {} ({}) VALUES {}",
66 iq.table,
67 cols,
68 rows.join(", ")
69 )
70}
71
72fn render_update(uq: &UpdateQuery) -> String {
73 let mut sql = format!("UPDATE {}", uq.table);
74 if let Some(target) = render_update_target(uq.target) {
75 sql.push(' ');
76 sql.push_str(target);
77 }
78 sql.push_str(" SET ");
79 let assignments = uq
80 .assignment_exprs
81 .iter()
82 .enumerate()
83 .map(|(idx, (column, expr))| {
84 let op = uq
85 .compound_assignment_ops
86 .get(idx)
87 .copied()
88 .flatten()
89 .map(render_compound_assignment_op)
90 .unwrap_or("=");
91 format!("{} {} {}", column, op, render_expr_sql(expr))
92 })
93 .collect::<Vec<_>>()
94 .join(", ");
95 sql.push_str(&assignments);
96 if let Some(filter) = &uq.filter {
97 sql.push_str(" WHERE ");
98 sql.push_str(&render_filter(filter));
99 }
100 if let Some(claim_limit) = uq.claim_limit {
101 if uq.claim_exact {
102 sql.push_str(" CLAIM EXACT ");
103 } else {
104 sql.push_str(" CLAIM LIMIT ");
105 }
106 sql.push_str(&claim_limit.to_string());
107 }
108 sql
109}
110
111fn render_update_target(target: UpdateTarget) -> Option<&'static str> {
112 match target {
113 UpdateTarget::Rows => None,
114 UpdateTarget::Documents => Some("DOCUMENTS"),
115 UpdateTarget::Kv => Some("KV"),
116 UpdateTarget::Nodes => Some("NODES"),
117 UpdateTarget::Edges => Some("EDGES"),
118 }
119}
120
121fn render_compound_assignment_op(op: BinOp) -> &'static str {
122 match op {
123 BinOp::Add => "+=",
124 BinOp::Sub => "-=",
125 BinOp::Mul => "*=",
126 BinOp::Div => "/=",
127 BinOp::Mod => "%=",
128 _ => "=",
129 }
130}
131
132fn render_queue_command(qc: &QueueCommand) -> String {
133 match qc {
134 QueueCommand::Push { queue, value, .. } => {
135 format!("QUEUE PUSH {} {}", queue, render_value_sql(value))
136 }
137 _ => String::new(),
138 }
139}
140
141fn render_expr_sql(expr: &Expr) -> String {
142 match expr {
143 Expr::Literal { value, .. } => render_value_sql(value),
144 Expr::Column { field, .. } => render_field_ref(field),
145 Expr::BinaryOp { op, lhs, rhs, .. } => format!(
146 "{} {} {}",
147 render_expr_sql(lhs),
148 render_bin_op(*op),
149 render_expr_sql(rhs)
150 ),
151 _ => "NULL".to_string(),
152 }
153}
154
155fn render_bin_op(op: BinOp) -> &'static str {
156 match op {
157 BinOp::Add => "+",
158 BinOp::Sub => "-",
159 BinOp::Mul => "*",
160 BinOp::Div => "/",
161 BinOp::Mod => "%",
162 BinOp::Eq => "=",
163 BinOp::Ne => "!=",
164 BinOp::Lt => "<",
165 BinOp::Le => "<=",
166 BinOp::Gt => ">",
167 BinOp::Ge => ">=",
168 BinOp::And => "AND",
169 BinOp::Or => "OR",
170 _ => "",
171 }
172}
173
174fn render_projection(p: &Projection) -> String {
175 match p {
176 Projection::All => "*".to_string(),
177 Projection::Column(col) => col.clone(),
178 Projection::Alias(col, alias) => format!("{} AS {}", col, alias),
179 Projection::Field(field, alias) => {
180 let col = render_field_ref(field);
181 match alias {
182 Some(a) => format!("{} AS {}", col, a),
183 None => col,
184 }
185 }
186 _ => "*".to_string(),
187 }
188}
189
190pub(crate) fn render_field_ref(f: &FieldRef) -> String {
191 match f {
192 FieldRef::TableColumn { table, column } if table.is_empty() => column.clone(),
193 FieldRef::TableColumn { table, column } => format!("{}.{}", table, column),
194 _ => "field".to_string(),
195 }
196}
197
198fn render_filter(filter: &Filter) -> String {
199 match filter {
200 Filter::Compare { field, op, value } => {
201 format!(
202 "{} {} {}",
203 render_field_ref(field),
204 op,
205 render_value_sql(value)
206 )
207 }
208 Filter::And(a, b) => format!("({}) AND ({})", render_filter(a), render_filter(b)),
209 Filter::Or(a, b) => format!("({}) OR ({})", render_filter(a), render_filter(b)),
210 _ => "1=1".to_string(),
211 }
212}
213
214pub fn render_value_sql(v: &Value) -> String {
220 match v {
221 Value::Null => "NULL".to_string(),
222 Value::Integer(i) => i.to_string(),
223 Value::UnsignedInteger(u) => u.to_string(),
224 Value::Float(f) => {
225 if f.fract() == 0.0 {
227 format!("{:.1}", f)
228 } else {
229 format!("{}", f)
230 }
231 }
232 Value::Boolean(b) => {
233 if *b {
234 "true".to_string()
235 } else {
236 "false".to_string()
237 }
238 }
239 Value::Text(s) => format!("'{}'", s.replace('\'', "''")),
240 Value::Json(bytes) => String::from_utf8_lossy(bytes).to_string(),
243 Value::Array(items) => {
247 let rendered: Vec<String> = items.iter().map(render_value_sql).collect();
248 format!("[{}]", rendered.join(", "))
249 }
250 _ => "NULL".to_string(),
251 }
252}