Skip to main content

nodedb_sql/
lib.rs

1//! nodedb-sql: SQL parser, planner, and optimizer for NodeDB.
2//!
3//! Parses SQL via sqlparser-rs, resolves against a catalog, and produces
4//! `SqlPlan` — an intermediate representation that both Origin (server)
5//! and Lite (embedded) map to their own execution model.
6//!
7//! ```text
8//! SQL → parse → resolve → plan → optimize → SqlPlan
9//! ```
10
11pub mod aggregate_walk;
12pub mod catalog;
13pub mod coerce;
14pub mod ddl_ast;
15pub mod dsl_bind;
16pub mod engine_rules;
17pub mod error;
18pub mod functions;
19pub mod optimizer;
20pub mod params;
21pub mod parser;
22pub mod planner;
23pub mod resolver;
24pub mod types;
25pub mod types_expr;
26
27pub use catalog::{SqlCatalog, SqlCatalogError};
28pub use error::{Result, SqlError};
29pub use params::ParamValue;
30pub use types::*;
31
32/// Parse a standalone SQL expression string into an `SqlExpr`.
33///
34/// Used by the DEFAULT expression evaluator to handle arbitrary expressions
35/// (e.g. `upper('x')`, `1 + 2`) that don't match the hard-coded keyword list.
36pub fn parse_expr_string(expr_text: &str) -> Result<SqlExpr> {
37    use sqlparser::dialect::GenericDialect;
38    use sqlparser::parser::Parser;
39
40    let dialect = GenericDialect {};
41    let ast_expr = Parser::new(&dialect)
42        .try_with_sql(expr_text)
43        .map_err(|e| SqlError::Parse {
44            detail: e.to_string(),
45        })?
46        .parse_expr()
47        .map_err(|e| SqlError::Parse {
48            detail: e.to_string(),
49        })?;
50
51    resolver::expr::convert_expr(&ast_expr)
52}
53
54use functions::registry::FunctionRegistry;
55use parser::preprocess;
56use parser::statement::{StatementKind, classify, parse_sql};
57
58/// Plan one or more SQL statements against the given catalog.
59///
60/// Handles NodeDB-specific syntax (UPSERT, `{ }` object literals) via
61/// pre-processing before handing to sqlparser.
62pub fn plan_sql(sql: &str, catalog: &dyn SqlCatalog) -> Result<Vec<SqlPlan>> {
63    let preprocessed = preprocess::preprocess(sql);
64    let effective_sql = preprocessed.as_ref().map_or(sql, |p| p.sql.as_str());
65    let is_upsert = preprocessed.as_ref().is_some_and(|p| p.is_upsert);
66
67    let statements = parse_sql(effective_sql)?;
68    plan_statements(&statements, is_upsert, catalog)
69}
70
71/// Plan SQL with bound parameters (prepared statement execution).
72///
73/// Parses the SQL (which may contain `$1`, `$2`, ... placeholders), substitutes
74/// placeholder AST nodes with concrete literal values from `params`, then plans
75/// normally. This avoids SQL text substitution entirely — parameters are bound
76/// at the AST level, not the string level.
77pub fn plan_sql_with_params(
78    sql: &str,
79    params: &[ParamValue],
80    catalog: &dyn SqlCatalog,
81) -> Result<Vec<SqlPlan>> {
82    let preprocessed = preprocess::preprocess(sql);
83    let effective_sql = preprocessed.as_ref().map_or(sql, |p| p.sql.as_str());
84    let is_upsert = preprocessed.as_ref().is_some_and(|p| p.is_upsert);
85
86    let mut statements = parse_sql(effective_sql)?;
87    for stmt in &mut statements {
88        params::bind_params(stmt, params);
89    }
90    plan_statements(&statements, is_upsert, catalog)
91}
92
93/// Plan a list of parsed statements.
94fn plan_statements(
95    statements: &[sqlparser::ast::Statement],
96    is_upsert: bool,
97    catalog: &dyn SqlCatalog,
98) -> Result<Vec<SqlPlan>> {
99    let functions = FunctionRegistry::new();
100    let mut plans = Vec::new();
101
102    for stmt in statements {
103        match classify(stmt) {
104            StatementKind::Select(query) => {
105                let plan = planner::select::plan_query(query, catalog, &functions)?;
106                let plan = optimizer::optimize(plan);
107                plans.push(plan);
108            }
109            StatementKind::Insert(ins) => {
110                let mut dml_plans = if is_upsert {
111                    planner::dml::plan_upsert(ins, catalog)?
112                } else {
113                    planner::dml::plan_insert(ins, catalog)?
114                };
115                plans.append(&mut dml_plans);
116            }
117            StatementKind::Update(stmt) => {
118                let mut update_plans = planner::dml::plan_update(stmt, catalog)?;
119                plans.append(&mut update_plans);
120            }
121            StatementKind::Delete(stmt) => {
122                let mut delete_plans = planner::dml::plan_delete(stmt, catalog)?;
123                plans.append(&mut delete_plans);
124            }
125            StatementKind::Truncate(stmt) => {
126                let mut trunc_plans = planner::dml::plan_truncate_stmt(stmt)?;
127                plans.append(&mut trunc_plans);
128            }
129            StatementKind::Other => {
130                return Err(SqlError::Unsupported {
131                    detail: format!("statement type: {stmt}"),
132                });
133            }
134        }
135    }
136
137    Ok(plans)
138}