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