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 engine_rules;
12pub mod error;
13pub mod functions;
14pub mod optimizer;
15pub mod params;
16pub mod parser;
17pub mod planner;
18pub mod resolver;
19pub mod types;
20
21pub use error::{Result, SqlError};
22pub use params::ParamValue;
23pub use types::*;
24
25use functions::registry::FunctionRegistry;
26use parser::preprocess;
27use parser::statement::{StatementKind, classify, parse_sql};
28
29/// Plan one or more SQL statements against the given catalog.
30///
31/// Handles NodeDB-specific syntax (UPSERT, `{ }` object literals) via
32/// pre-processing before handing to sqlparser.
33pub fn plan_sql(sql: &str, catalog: &dyn SqlCatalog) -> Result<Vec<SqlPlan>> {
34    let preprocessed = preprocess::preprocess(sql);
35    let effective_sql = preprocessed.as_ref().map_or(sql, |p| p.sql.as_str());
36    let is_upsert = preprocessed.as_ref().is_some_and(|p| p.is_upsert);
37
38    let statements = parse_sql(effective_sql)?;
39    plan_statements(&statements, is_upsert, catalog)
40}
41
42/// Plan SQL with bound parameters (prepared statement execution).
43///
44/// Parses the SQL (which may contain `$1`, `$2`, ... placeholders), substitutes
45/// placeholder AST nodes with concrete literal values from `params`, then plans
46/// normally. This avoids SQL text substitution entirely — parameters are bound
47/// at the AST level, not the string level.
48pub fn plan_sql_with_params(
49    sql: &str,
50    params: &[ParamValue],
51    catalog: &dyn SqlCatalog,
52) -> Result<Vec<SqlPlan>> {
53    let preprocessed = preprocess::preprocess(sql);
54    let effective_sql = preprocessed.as_ref().map_or(sql, |p| p.sql.as_str());
55    let is_upsert = preprocessed.as_ref().is_some_and(|p| p.is_upsert);
56
57    let mut statements = parse_sql(effective_sql)?;
58    for stmt in &mut statements {
59        params::bind_params(stmt, params);
60    }
61    plan_statements(&statements, is_upsert, catalog)
62}
63
64/// Plan a list of parsed statements.
65fn plan_statements(
66    statements: &[sqlparser::ast::Statement],
67    is_upsert: bool,
68    catalog: &dyn SqlCatalog,
69) -> Result<Vec<SqlPlan>> {
70    let functions = FunctionRegistry::new();
71    let mut plans = Vec::new();
72
73    for stmt in statements {
74        match classify(stmt) {
75            StatementKind::Select(query) => {
76                let plan = planner::select::plan_query(query, catalog, &functions)?;
77                let plan = optimizer::optimize(plan);
78                plans.push(plan);
79            }
80            StatementKind::Insert(ins) => {
81                let mut dml_plans = if is_upsert {
82                    planner::dml::plan_upsert(ins, catalog)?
83                } else {
84                    planner::dml::plan_insert(ins, catalog)?
85                };
86                plans.append(&mut dml_plans);
87            }
88            StatementKind::Update(stmt) => {
89                let mut update_plans = planner::dml::plan_update(stmt, catalog)?;
90                plans.append(&mut update_plans);
91            }
92            StatementKind::Delete(stmt) => {
93                let mut delete_plans = planner::dml::plan_delete(stmt, catalog)?;
94                plans.append(&mut delete_plans);
95            }
96            StatementKind::Truncate(stmt) => {
97                let mut trunc_plans = planner::dml::plan_truncate_stmt(stmt)?;
98                plans.append(&mut trunc_plans);
99            }
100            StatementKind::Other => {
101                return Err(SqlError::Unsupported {
102                    detail: format!("statement type: {stmt}"),
103                });
104            }
105        }
106    }
107
108    Ok(plans)
109}