Skip to main content

reddb_server/storage/query/
renderer.rs

1//! AST → SQL/RQL renderer (partial subset for property round-trip tests).
2//!
3//! Covers the three query categories exercised by the property test:
4//! - `SELECT col, … FROM table [WHERE simple-filter]`
5//! - `INSERT INTO table (cols) VALUES (vals)`
6//! - `QUEUE PUSH queue value`
7//!
8//! Run the round-trip property test with:
9//! ```text
10//! cargo test -p reddb-server property_round_trip
11//! ```
12
13use crate::storage::query::ast::{
14    CompareOp, FieldRef, Filter, InsertQuery, Projection, QueryExpr, QueueCommand, TableQuery,
15};
16use crate::storage::schema::Value;
17
18/// Render a `QueryExpr` back into canonical SQL/RQL.
19///
20/// Returns an empty string for variants outside the supported subset.
21pub fn render(expr: &QueryExpr) -> String {
22    match expr {
23        QueryExpr::Table(tq) => render_table(tq),
24        QueryExpr::Insert(iq) => render_insert(iq),
25        QueryExpr::QueueCommand(qc) => render_queue_command(qc),
26        _ => String::new(),
27    }
28}
29
30fn render_table(tq: &TableQuery) -> String {
31    let cols = if tq.columns.is_empty() {
32        "*".to_string()
33    } else {
34        tq.columns
35            .iter()
36            .map(render_projection)
37            .collect::<Vec<_>>()
38            .join(", ")
39    };
40    let mut sql = format!("SELECT {} FROM {}", cols, tq.table);
41    if let Some(filter) = &tq.filter {
42        sql.push_str(" WHERE ");
43        sql.push_str(&render_filter(filter));
44    }
45    sql
46}
47
48fn render_insert(iq: &InsertQuery) -> String {
49    let cols = iq.columns.join(", ");
50    let rows: Vec<String> = iq
51        .values
52        .iter()
53        .map(|row| {
54            let vals = row
55                .iter()
56                .map(render_value_sql)
57                .collect::<Vec<_>>()
58                .join(", ");
59            format!("({})", vals)
60        })
61        .collect();
62    format!(
63        "INSERT INTO {} ({}) VALUES {}",
64        iq.table,
65        cols,
66        rows.join(", ")
67    )
68}
69
70fn render_queue_command(qc: &QueueCommand) -> String {
71    match qc {
72        QueueCommand::Push { queue, value, .. } => {
73            format!("QUEUE PUSH {} {}", queue, render_value_sql(value))
74        }
75        _ => String::new(),
76    }
77}
78
79fn render_projection(p: &Projection) -> String {
80    match p {
81        Projection::All => "*".to_string(),
82        Projection::Column(col) => col.clone(),
83        Projection::Alias(col, alias) => format!("{} AS {}", col, alias),
84        Projection::Field(field, alias) => {
85            let col = render_field_ref(field);
86            match alias {
87                Some(a) => format!("{} AS {}", col, a),
88                None => col,
89            }
90        }
91        _ => "*".to_string(),
92    }
93}
94
95pub(crate) fn render_field_ref(f: &FieldRef) -> String {
96    match f {
97        FieldRef::TableColumn { table, column } if table.is_empty() => column.clone(),
98        FieldRef::TableColumn { table, column } => format!("{}.{}", table, column),
99        _ => "field".to_string(),
100    }
101}
102
103fn render_filter(filter: &Filter) -> String {
104    match filter {
105        Filter::Compare { field, op, value } => {
106            format!(
107                "{} {} {}",
108                render_field_ref(field),
109                op,
110                render_value_sql(value)
111            )
112        }
113        Filter::And(a, b) => format!("({}) AND ({})", render_filter(a), render_filter(b)),
114        Filter::Or(a, b) => format!("({}) OR ({})", render_filter(a), render_filter(b)),
115        _ => "1=1".to_string(),
116    }
117}
118
119/// Render a `Value` as a SQL literal suitable for embedding in a query string.
120/// Only the subset used by property tests is handled; others fall back to NULL.
121pub(crate) fn render_value_sql(v: &Value) -> String {
122    match v {
123        Value::Null => "NULL".to_string(),
124        Value::Integer(i) => i.to_string(),
125        Value::UnsignedInteger(u) => u.to_string(),
126        Value::Float(f) => {
127            // Ensure the rendered form parses back as Float, not Integer.
128            if f.fract() == 0.0 {
129                format!("{:.1}", f)
130            } else {
131                format!("{}", f)
132            }
133        }
134        Value::Boolean(b) => {
135            if *b {
136                "true".to_string()
137            } else {
138                "false".to_string()
139            }
140        }
141        Value::Text(s) => format!("'{}'", s.replace('\'', "''")),
142        // JSON bytes are stored as canonical compact JSON; emit them raw so
143        // the lexer picks them up as a JsonLiteral token on re-parse.
144        Value::Json(bytes) => String::from_utf8_lossy(bytes).to_string(),
145        _ => "NULL".to_string(),
146    }
147}