Skip to main content

nodedb_sql/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! nodedb-sql: SQL parser, planner, and optimizer for NodeDB.
4//!
5//! Parses SQL via sqlparser-rs, resolves against a catalog, and produces
6//! `SqlPlan` — an intermediate representation that both Origin (server)
7//! and Lite (embedded) map to their own execution model.
8//!
9//! ```text
10//! SQL → parse → resolve → plan → optimize → SqlPlan
11//! ```
12
13pub mod aggregate_walk;
14pub mod catalog;
15pub mod coerce;
16pub mod ddl_ast;
17pub mod dsl_bind;
18pub mod engine_rules;
19pub mod error;
20pub mod fts_types;
21pub mod functions;
22pub mod optimizer;
23pub mod params;
24pub mod parser;
25pub mod planner;
26pub mod reserved;
27pub mod resolver;
28pub mod temporal;
29pub mod types;
30pub mod types_array;
31pub mod types_expr;
32pub mod visitor;
33
34pub use temporal::{TemporalScope, ValidTime};
35pub use visitor::PlanVisitor;
36pub use visitor::dispatch;
37
38pub use catalog::{SqlCatalog, SqlCatalogError};
39pub use error::{Result, SqlError};
40pub use params::ParamValue;
41pub use types::*;
42
43/// Parse a standalone SQL expression string into an `SqlExpr`.
44///
45/// Used by the DEFAULT expression evaluator to handle arbitrary expressions
46/// (e.g. `upper('x')`, `1 + 2`) that don't match the hard-coded keyword list.
47pub fn parse_expr_string(expr_text: &str) -> Result<SqlExpr> {
48    use sqlparser::dialect::GenericDialect;
49    use sqlparser::parser::Parser;
50
51    let dialect = GenericDialect {};
52    let ast_expr = Parser::new(&dialect)
53        .try_with_sql(expr_text)
54        .map_err(|e| SqlError::Parse {
55            detail: e.to_string(),
56        })?
57        .parse_expr()
58        .map_err(|e| SqlError::Parse {
59            detail: e.to_string(),
60        })?;
61
62    resolver::expr::convert_expr(&ast_expr)
63}
64
65use functions::registry::FunctionRegistry;
66use parser::array_stmt::{ArrayStatement, try_parse_array_statement};
67use parser::preprocess;
68use parser::statement::{StatementKind, classify, parse_sql};
69
70/// Plan one or more SQL statements against the given catalog.
71///
72/// Handles NodeDB-specific syntax (UPSERT, `{ }` object literals) via
73/// pre-processing before handing to sqlparser.
74pub fn plan_sql(sql: &str, catalog: &dyn SqlCatalog) -> Result<Vec<SqlPlan>> {
75    // Array DDL/DML uses non-standard syntax that sqlparser-rs cannot
76    // accept. Intercept it before the standard preprocess pipeline.
77    if let Some(stmt) = try_parse_array_statement(sql)? {
78        return plan_array_statement(stmt, catalog);
79    }
80    let preprocessed = preprocess::preprocess(sql)?;
81    let effective_sql = preprocessed.as_ref().map_or(sql, |p| p.sql.as_str());
82    let is_upsert = preprocessed.as_ref().is_some_and(|p| p.is_upsert);
83    let temporal = preprocessed
84        .as_ref()
85        .map(|p| p.temporal)
86        .unwrap_or_default();
87
88    let statements = parse_sql(effective_sql)?;
89    plan_statements(&statements, is_upsert, temporal, catalog)
90}
91
92/// Plan SQL with bound parameters (prepared statement execution).
93///
94/// Parses the SQL (which may contain `$1`, `$2`, ... placeholders), substitutes
95/// placeholder AST nodes with concrete literal values from `params`, then plans
96/// normally. This avoids SQL text substitution entirely — parameters are bound
97/// at the AST level, not the string level.
98pub fn plan_sql_with_params(
99    sql: &str,
100    params: &[ParamValue],
101    catalog: &dyn SqlCatalog,
102) -> Result<Vec<SqlPlan>> {
103    // Array DDL/DML never carries `$N` placeholders, but be defensive:
104    // intercept the same way as the no-params path so the array surface
105    // is reachable even if a client uses extended-query mode.
106    if let Some(stmt) = try_parse_array_statement(sql)? {
107        let _ = params; // arrays don't accept bound params today
108        return plan_array_statement(stmt, catalog);
109    }
110    let preprocessed = preprocess::preprocess(sql)?;
111    let effective_sql = preprocessed.as_ref().map_or(sql, |p| p.sql.as_str());
112    let is_upsert = preprocessed.as_ref().is_some_and(|p| p.is_upsert);
113    let temporal = preprocessed
114        .as_ref()
115        .map(|p| p.temporal)
116        .unwrap_or_default();
117
118    let mut statements = parse_sql(effective_sql)?;
119    for stmt in &mut statements {
120        params::bind_params(stmt, params);
121    }
122    plan_statements(&statements, is_upsert, temporal, catalog)
123}
124
125/// Plan a list of parsed statements.
126fn plan_statements(
127    statements: &[sqlparser::ast::Statement],
128    is_upsert: bool,
129    temporal: TemporalScope,
130    catalog: &dyn SqlCatalog,
131) -> Result<Vec<SqlPlan>> {
132    let functions = FunctionRegistry::new();
133    let mut plans = Vec::new();
134
135    for stmt in statements {
136        match classify(stmt) {
137            StatementKind::Select(query) => {
138                let plan = planner::select::plan_query(query, catalog, &functions, temporal)?;
139                let plan = optimizer::optimize(plan);
140                plans.push(plan);
141            }
142            StatementKind::Insert(ins) => {
143                let mut dml_plans = if is_upsert {
144                    planner::dml::plan_upsert(ins, catalog)?
145                } else {
146                    planner::dml::plan_insert(ins, catalog)?
147                };
148                plans.append(&mut dml_plans);
149            }
150            StatementKind::Update(stmt) => {
151                let mut update_plans = planner::dml::plan_update(stmt, catalog)?;
152                plans.append(&mut update_plans);
153            }
154            StatementKind::Delete(stmt) => {
155                let mut delete_plans = planner::dml::plan_delete(stmt, catalog)?;
156                plans.append(&mut delete_plans);
157            }
158            StatementKind::Truncate(stmt) => {
159                let mut trunc_plans = planner::dml::plan_truncate_stmt(stmt)?;
160                plans.append(&mut trunc_plans);
161            }
162            StatementKind::Merge(stmt) => {
163                let mut merge_plans = planner::merge::plan_merge(stmt, catalog)?;
164                plans.append(&mut merge_plans);
165            }
166            StatementKind::CreateIndex(ci) => {
167                plans.push(planner::index_ddl::plan_create_index(ci)?);
168            }
169            StatementKind::DropIndex(stmt) => {
170                plans.push(planner::index_ddl::plan_drop_index(stmt)?);
171            }
172            StatementKind::Other => {
173                return Err(SqlError::Unsupported {
174                    detail: format!("statement type: {stmt}"),
175                });
176            }
177        }
178    }
179
180    Ok(plans)
181}
182
183/// Plan one parsed array statement.
184fn plan_array_statement(stmt: ArrayStatement, catalog: &dyn SqlCatalog) -> Result<Vec<SqlPlan>> {
185    match stmt {
186        ArrayStatement::Create(c) => Ok(vec![planner::array_ddl::plan_create_array(&c)?]),
187        ArrayStatement::Drop(d) => Ok(vec![planner::array_ddl::plan_drop_array(&d)?]),
188        ArrayStatement::Insert(i) => planner::array_dml::plan_insert_array(&i, catalog),
189        ArrayStatement::Delete(d) => planner::array_dml::plan_delete_array(&d, catalog),
190        ArrayStatement::Alter(a) => Ok(vec![planner::array_ddl::plan_alter_array(&a)?]),
191    }
192}