use crate::ast::{
BinOp, CompareOp, DeleteQuery, Expr, FieldRef, Filter, InsertQuery, Projection, QueryExpr,
QueueCommand, TableQuery, UpdateQuery, UpdateTarget,
};
use reddb_types::types::Value;
pub fn render(expr: &QueryExpr) -> String {
match expr {
QueryExpr::Table(tq) => render_table(tq),
QueryExpr::Insert(iq) => render_insert(iq),
QueryExpr::Update(uq) => render_update(uq),
QueryExpr::Delete(dq) => render_delete(dq),
QueryExpr::QueueCommand(qc) => render_queue_command(qc),
QueryExpr::Explain(explain) => render_explain(explain.inner.as_ref()),
_ => String::new(),
}
}
pub fn render_identifier(identifier: &str) -> String {
let mut lexer = crate::lexer::Lexer::new(identifier);
let contextual_keyword = matches!(
identifier.to_ascii_uppercase().as_str(),
"CASE" | "CONSTRAINT" | "CURRENT_TIMESTAMP" | "CURRENT_DATE" | "CURRENT_TIME"
);
let plain = !contextual_keyword
&& matches!(lexer.next_token(), Ok(token) if token.token == crate::lexer::Token::Ident(identifier.to_string()))
&& matches!(lexer.next_token(), Ok(token) if token.token == crate::lexer::Token::Eof);
if plain {
identifier.to_string()
} else {
format!(
"\"{}\"",
identifier.replace('\\', "\\\\").replace('"', "\"\"")
)
}
}
fn render_identifier_path(path: &str) -> String {
path.split('.')
.map(render_identifier)
.collect::<Vec<_>>()
.join(".")
}
fn render_explain(inner: &QueryExpr) -> String {
let rendered = render(inner);
if rendered.is_empty() {
String::new()
} else {
format!("EXPLAIN {rendered}")
}
}
fn render_table(tq: &TableQuery) -> String {
let cols = if tq.columns.is_empty() {
"*".to_string()
} else {
tq.columns
.iter()
.map(render_projection)
.collect::<Vec<_>>()
.join(", ")
};
let mut sql = format!("SELECT {} FROM {}", cols, render_identifier_path(&tq.table));
if let Some(filter) = &tq.filter {
sql.push_str(" WHERE ");
sql.push_str(&render_filter(filter));
}
sql
}
fn render_insert(iq: &InsertQuery) -> String {
let cols = iq
.columns
.iter()
.map(|name| render_identifier(name))
.collect::<Vec<_>>()
.join(", ");
let rows: Vec<String> = iq
.values
.iter()
.map(|row| {
let vals = row
.iter()
.map(render_value_sql)
.collect::<Vec<_>>()
.join(", ");
format!("({})", vals)
})
.collect();
let mut sql = format!(
"INSERT INTO {} ({}) VALUES {}",
render_identifier_path(&iq.table),
cols,
rows.join(", ")
);
if let Some(clause) = &iq.on_conflict {
sql.push_str(" ON CONFLICT");
if let Some(columns) = &clause.target {
sql.push_str(" (");
sql.push_str(
&columns
.iter()
.map(|column| render_identifier(column))
.collect::<Vec<_>>()
.join(", "),
);
sql.push(')');
}
match &clause.action {
crate::ast::OnConflictAction::DoNothing => sql.push_str(" DO NOTHING"),
crate::ast::OnConflictAction::DoUpdate { assignments } => {
sql.push_str(" DO UPDATE SET ");
sql.push_str(
&assignments
.iter()
.map(|(column, expr)| {
format!("{} = {}", render_identifier(column), render_expr_sql(expr))
})
.collect::<Vec<_>>()
.join(", "),
);
}
}
}
sql
}
fn render_update(uq: &UpdateQuery) -> String {
let mut sql = format!("UPDATE {}", render_identifier_path(&uq.table));
if let Some(target) = render_update_target(uq.target) {
sql.push(' ');
sql.push_str(target);
}
sql.push_str(" SET ");
let assignments = uq
.assignment_exprs
.iter()
.enumerate()
.map(|(idx, (column, expr))| {
let op = uq
.compound_assignment_ops
.get(idx)
.copied()
.flatten()
.map(render_compound_assignment_op)
.unwrap_or("=");
format!(
"{} {} {}",
render_identifier_path(column),
op,
render_expr_sql(expr)
)
})
.collect::<Vec<_>>()
.join(", ");
sql.push_str(&assignments);
if let Some(filter) = &uq.filter {
sql.push_str(" WHERE ");
sql.push_str(&render_filter(filter));
}
if let Some(claim_limit) = uq.claim_limit {
if uq.claim_exact {
sql.push_str(" CLAIM EXACT ");
} else {
sql.push_str(" CLAIM LIMIT ");
}
sql.push_str(&claim_limit.to_string());
}
sql
}
fn render_delete(dq: &DeleteQuery) -> String {
let mut sql = format!("DELETE FROM {}", render_identifier_path(&dq.table));
if let Some(filter) = &dq.filter {
sql.push_str(" WHERE ");
sql.push_str(&render_filter(filter));
}
sql
}
fn render_update_target(target: UpdateTarget) -> Option<&'static str> {
match target {
UpdateTarget::Rows | UpdateTarget::Documents | UpdateTarget::Kv => None,
UpdateTarget::Nodes => Some("NODES"),
UpdateTarget::Edges => Some("EDGES"),
}
}
fn render_compound_assignment_op(op: BinOp) -> &'static str {
match op {
BinOp::Add => "+=",
BinOp::Sub => "-=",
BinOp::Mul => "*=",
BinOp::Div => "/=",
BinOp::Mod => "%=",
_ => "=",
}
}
fn render_queue_command(qc: &QueueCommand) -> String {
match qc {
QueueCommand::Push { queue, value, .. } => {
format!(
"QUEUE PUSH {} {}",
render_identifier(queue),
render_value_sql(value)
)
}
_ => String::new(),
}
}
fn render_expr_sql(expr: &Expr) -> String {
match expr {
Expr::Literal { value, .. } => render_value_sql(value),
Expr::Column { field, .. } => render_field_ref(field),
Expr::BinaryOp { op, lhs, rhs, .. } => format!(
"{} {} {}",
render_expr_sql(lhs),
render_bin_op(*op),
render_expr_sql(rhs)
),
_ => "NULL".to_string(),
}
}
fn render_bin_op(op: BinOp) -> &'static str {
match op {
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::Div => "/",
BinOp::Mod => "%",
BinOp::Eq => "=",
BinOp::Ne => "!=",
BinOp::Lt => "<",
BinOp::Le => "<=",
BinOp::Gt => ">",
BinOp::Ge => ">=",
BinOp::And => "AND",
BinOp::Or => "OR",
_ => "",
}
}
fn render_projection(p: &Projection) -> String {
match p {
Projection::All => "*".to_string(),
Projection::Column(col) => render_identifier(col),
Projection::Alias(col, alias) => {
format!("{} AS {}", render_identifier(col), render_identifier(alias))
}
Projection::Field(field, alias) => {
let col = render_field_ref(field);
match alias {
Some(a) => format!("{} AS {}", col, render_identifier(a)),
None => col,
}
}
_ => "*".to_string(),
}
}
pub(crate) fn render_field_ref(f: &FieldRef) -> String {
match f {
FieldRef::TableColumn { table, column } if table.is_empty() => {
render_identifier_path(column)
}
FieldRef::TableColumn { table, column } => {
format!(
"{}.{}",
render_identifier_path(table),
render_identifier_path(column)
)
}
_ => "field".to_string(),
}
}
fn render_filter(filter: &Filter) -> String {
match filter {
Filter::Compare { field, op, value } => {
format!(
"{} {} {}",
render_field_ref(field),
op,
render_value_sql(value)
)
}
Filter::And(a, b) => format!("({}) AND ({})", render_filter(a), render_filter(b)),
Filter::Or(a, b) => format!("({}) OR ({})", render_filter(a), render_filter(b)),
_ => "1=1".to_string(),
}
}
pub fn render_value_sql(v: &Value) -> String {
match v {
Value::Null => "NULL".to_string(),
Value::Integer(i) => i.to_string(),
Value::UnsignedInteger(u) => u.to_string(),
Value::Float(f) => {
if f.fract() == 0.0 {
format!("{:.1}", f)
} else {
format!("{}", f)
}
}
Value::Boolean(b) => {
if *b {
"true".to_string()
} else {
"false".to_string()
}
}
Value::Text(s) => format!("'{}'", s.replace('\'', "''")),
Value::Json(bytes) => String::from_utf8_lossy(bytes).to_string(),
Value::Array(items) => {
let rendered: Vec<String> = items.iter().map(render_value_sql).collect();
format!("[{}]", rendered.join(", "))
}
_ => "NULL".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::render;
#[test]
fn insert_on_conflict_round_trips_through_the_canonical_renderer() {
let sql = "INSERT INTO counters (name, value) VALUES ('jobs', 3) ON CONFLICT (name) DO UPDATE SET value = value + EXCLUDED.value";
let parsed = crate::parser::parse(sql).expect("ON CONFLICT should parse");
assert_eq!(render(&parsed.query), sql);
}
}