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