Skip to main content

reddb_engine/query/
scalar.rs

1//! Experimental storage-free SELECT execution using the native scalar evaluator.
2//! Storage-backed statements must go through the full runtime, not this entrypoint.
3#![deny(
4    clippy::unwrap_used,
5    clippy::too_many_lines,
6    clippy::cast_possible_truncation
7)]
8
9use reddb_rql::ast::{FieldRef, QueryExpr, SelectItem};
10use reddb_rql::sql_lowering::effective_table_projections;
11use reddb_types::Value;
12
13use super::evaluator;
14use super::labels::projection_name;
15
16#[derive(Debug)]
17pub struct ScalarResult {
18    pub columns: Vec<String>,
19    pub rows: Vec<Vec<Value>>,
20}
21
22impl ScalarResult {
23    /// Preserve RedDB's exact integer and decimal tags at the JS/JSON boundary.
24    pub fn to_json(&self) -> reddb_types::json::Value {
25        reddb_types::json!({
26            "columns": self.columns,
27            "rows": self.rows.iter().map(|row| {
28                row.iter().map(Value::to_json).collect::<Vec<_>>()
29            }).collect::<Vec<_>>()
30        })
31    }
32}
33
34#[derive(Debug)]
35pub enum ScalarError {
36    Parse(reddb_rql::ParseError),
37    Unsupported,
38    Evaluation(evaluator::EvalError),
39}
40
41impl std::fmt::Display for ScalarError {
42    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::Parse(error) => error.fmt(formatter),
45            Self::Unsupported => formatter.write_str(
46                "only storage-free scalar SELECT statements are available in this experimental executor",
47            ),
48            Self::Evaluation(error) => error.fmt(formatter),
49        }
50    }
51}
52
53impl std::error::Error for ScalarError {}
54
55/// Execute a bounded, canonical RQL scalar statement. Unsupported clauses are
56/// rejected explicitly so they cannot silently turn into unconditional reads.
57pub fn execute(sql: &str) -> Result<ScalarResult, ScalarError> {
58    let statement = reddb_rql::parse_frontend(sql).map_err(ScalarError::Parse)?;
59    let QueryExpr::Table(query) = statement.into_query_expr() else {
60        return Err(ScalarError::Unsupported);
61    };
62    if query.table != "any"
63        || query.source.is_some()
64        || query.alias.is_some()
65        || query.select_items.is_empty()
66        || query.where_expr.is_some()
67        || query.filter.is_some()
68        || !query.group_by_exprs.is_empty()
69        || !query.group_by.is_empty()
70        || query.having_expr.is_some()
71        || query.having.is_some()
72        || !query.order_by.is_empty()
73        || query.limit_param.is_some()
74        || query.offset_param.is_some()
75        || query.expand.is_some()
76        || query.as_of.is_some()
77        || query.sessionize.is_some()
78    {
79        return Err(ScalarError::Unsupported);
80    }
81    let projections = effective_table_projections(&query);
82    let empty_row = |_: &FieldRef| None;
83    let mut values = Vec::with_capacity(query.select_items.len());
84    for item in &query.select_items {
85        let SelectItem::Expr { expr, .. } = item else {
86            return Err(ScalarError::Unsupported);
87        };
88        values.push(evaluator::evaluate(expr, &empty_row).map_err(ScalarError::Evaluation)?);
89    }
90    let rows = if query.limit == Some(0) || query.offset.is_some_and(|offset| offset > 0) {
91        Vec::new()
92    } else {
93        vec![values]
94    };
95    Ok(ScalarResult {
96        columns: projections.iter().map(projection_name).collect(),
97        rows,
98    })
99}