Skip to main content

oxisql_parse/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3
4//! `oxisql-parse` — SQL parsing utilities for OxiSQL.
5//!
6//! Re-exports the [`sqlparser`] AST types most commonly needed by OxiSQL
7//! backends and provides convenience functions for parsing, formatting,
8//! analysing, and dialect-specific handling of SQL statements.
9//!
10//! # Quick start
11//!
12//! ```rust
13//! use oxisql_parse::{parse, parse_one, is_read_only};
14//!
15//! let stmts = parse("SELECT 1; SELECT 2").unwrap();
16//! assert_eq!(stmts.len(), 2);
17//!
18//! let stmt = parse_one("SELECT 42").unwrap();
19//! assert!(is_read_only(&stmt));
20//! ```
21
22// ── Optimizer module ─────────────────────────────────────────────────────────
23pub mod optimizer;
24pub use optimizer::expr_util::{
25    canonical_hash, collect_colrefs, equi_key, find_common_subexprs, join_conjuncts, parse_expr,
26    parse_predicate, render, split_conjuncts, ColRef,
27};
28pub use optimizer::{
29    CommonSubexprElimination, ConstantFolding, JoinAlgoHint, JoinAlgorithmPass, LimitPushThrough,
30    OptPass, Optimizer, PredicatePushdown, PredicateSimplification, ProjectionPruning,
31};
32
33// ── Decorrelation ─────────────────────────────────────────────────────────────
34pub mod decorrelate;
35pub use decorrelate::PlannerOptions;
36
37// ── Parameterization ──────────────────────────────────────────────────────────
38pub mod parameterize;
39pub use parameterize::{parameterize, ParameterizedSql};
40
41// ── Plan cache ────────────────────────────────────────────────────────────────
42pub mod plan_cache;
43pub use plan_cache::PlanCache;
44// ── Aggregate helpers ─────────────────────────────────────────────────────────
45pub mod agg;
46pub use agg::{extract_aggregates, is_aggregate_expr, AggFunc, AggregateExpr};
47// ── DML / Validator / Cost / Builder modules ─────────────────────────────────
48pub mod dml;
49pub use dml::{plan_dml, DmlPlan};
50pub mod validate;
51pub use validate::{SchemaValidator, ValidationError};
52pub mod cost;
53pub use cost::{ColumnStats, CostEstimate, CostModel, NodeCost, TableStats};
54pub mod builder;
55mod columns;
56mod setops;
57mod window;
58pub use builder::QueryBuilder;
59
60// ── Plan types ───────────────────────────────────────────────────────────────
61pub mod plan;
62pub use plan::{JoinType, LogicalPlan, SetOpType, SortExpr, WindowFunctionDef};
63
64// ── Planner ──────────────────────────────────────────────────────────────────
65pub mod planner;
66pub use planner::{plan_query, plan_statement, plan_statement_with_opts};
67
68// ── Plan explanation ─────────────────────────────────────────────────────────
69pub mod explain;
70pub use explain::{explain, explain_json, explain_verbose};
71
72// ── Analysis utilities ────────────────────────────────────────────────────────
73pub mod analysis;
74pub use analysis::{count_params, extract_columns, extract_tables, is_read_only, normalize};
75
76// ── LRU parse cache ──────────────────────────────────────────────────────────
77pub mod cache;
78pub use cache::ParseCache;
79
80// ── Re-exports ──────────────────────────────────────────────────────────────
81use oxisql_core::OxiSqlError;
82pub use sqlparser::ast::{Expr, JoinConstraint, ObjectName, SelectItem, Statement, TableFactor};
83use sqlparser::dialect::Dialect;
84pub use sqlparser::dialect::{GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
85use sqlparser::parser::Parser;
86
87// ── SQL Dialect enum ────────────────────────────────────────────────────────
88/// SQL dialect selection for parsing.
89///
90/// Different databases have dialect-specific syntax; selecting the correct
91/// dialect improves parse accuracy for backend-specific SQL.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum SqlDialect {
94    /// ANSI / generic SQL dialect.
95    Generic,
96    /// PostgreSQL dialect (supports `::` casts, `$1` params, etc.).
97    Postgres,
98    /// MySQL dialect (supports backtick identifiers, etc.).
99    MySQL,
100    /// SQLite dialect (supports `AUTOINCREMENT`, backtick/bracket identifiers, etc.).
101    SQLite,
102}
103
104impl SqlDialect {
105    /// Return the `sqlparser` [`Dialect`] implementation for this variant.
106    pub(crate) fn to_sqlparser(self) -> Box<dyn Dialect> {
107        match self {
108            SqlDialect::Generic => Box::new(GenericDialect {}),
109            SqlDialect::Postgres => Box::new(PostgreSqlDialect {}),
110            SqlDialect::MySQL => Box::new(MySqlDialect {}),
111            SqlDialect::SQLite => Box::new(SQLiteDialect {}),
112        }
113    }
114}
115
116// ── Optimizer convenience wrapper ────────────────────────────────────────────
117
118/// Apply the default four-pass optimizer to `plan` and return the result.
119///
120/// This is a convenience shorthand for `Optimizer::new().optimize(plan)`.
121/// The passes run are: predicate pushdown, projection pruning (no-op when no
122/// required columns are specified), constant folding, and limit push-through.
123pub fn optimize(plan: LogicalPlan) -> LogicalPlan {
124    optimizer::optimize(plan)
125}
126
127// ── Dialect convenience wrappers ─────────────────────────────────────────────
128
129/// Parse SQL using the PostgreSQL dialect.
130///
131/// # Errors
132///
133/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
134pub fn parse_postgres(sql: &str) -> Result<Vec<Statement>, OxiSqlError> {
135    parse_with_dialect(sql, SqlDialect::Postgres)
136}
137
138/// Parse SQL using the MySQL dialect.
139///
140/// # Errors
141///
142/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
143pub fn parse_mysql(sql: &str) -> Result<Vec<Statement>, OxiSqlError> {
144    parse_with_dialect(sql, SqlDialect::MySQL)
145}
146
147/// Parse SQL using the SQLite dialect.
148///
149/// # Errors
150///
151/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
152pub fn parse_sqlite(sql: &str) -> Result<Vec<Statement>, OxiSqlError> {
153    parse_with_dialect(sql, SqlDialect::SQLite)
154}
155
156// ── Parsing functions ───────────────────────────────────────────────────────
157
158/// Parse a SQL string into a list of [`Statement`]s using the generic SQL
159/// dialect.
160///
161/// # Errors
162///
163/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
164pub fn parse(sql: &str) -> Result<Vec<Statement>, OxiSqlError> {
165    let dialect = GenericDialect {};
166    Parser::parse_sql(&dialect, sql).map_err(|e| OxiSqlError::Parse(e.to_string()))
167}
168
169/// Parse a SQL string using a specific [`SqlDialect`].
170///
171/// # Errors
172///
173/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
174pub fn parse_with_dialect(sql: &str, dialect: SqlDialect) -> Result<Vec<Statement>, OxiSqlError> {
175    let d = dialect.to_sqlparser();
176    Parser::parse_sql(d.as_ref(), sql).map_err(|e| OxiSqlError::Parse(e.to_string()))
177}
178
179/// Parse a SQL string that is expected to contain exactly one statement.
180///
181/// Returns [`OxiSqlError::Parse`] if parsing fails or if the string contains
182/// zero or more than one statement.
183pub fn parse_one(sql: &str) -> Result<Statement, OxiSqlError> {
184    let mut stmts = parse(sql)?;
185    if stmts.len() != 1 {
186        return Err(OxiSqlError::Parse(format!(
187            "expected exactly 1 statement, got {}",
188            stmts.len()
189        )));
190    }
191    Ok(stmts.remove(0))
192}
193
194/// Parse a single statement using a specific dialect.
195pub fn parse_one_with_dialect(sql: &str, dialect: SqlDialect) -> Result<Statement, OxiSqlError> {
196    let mut stmts = parse_with_dialect(sql, dialect)?;
197    if stmts.len() != 1 {
198        return Err(OxiSqlError::Parse(format!(
199            "expected exactly 1 statement, got {}",
200            stmts.len()
201        )));
202    }
203    Ok(stmts.remove(0))
204}
205
206// ── Formatting ──────────────────────────────────────────────────────────────
207
208/// Convert a parsed AST statement back to a SQL string.
209///
210/// Uses sqlparser's `Display` implementation for round-trip formatting.
211pub fn format(stmt: &Statement) -> String {
212    stmt.to_string()
213}
214
215// ── plan_query_with convenience wrapper ──────────────────────────────────────
216
217/// Parse `sql`, then plan it using the given [`PlannerOptions`].
218///
219/// This is the top-level entry point for callers that want to control
220/// decorrelation behaviour without going through a [`PlanCache`].
221///
222/// # Errors
223///
224/// Returns [`OxiSqlError::Parse`] if parsing or planning fails.
225pub fn plan_query_with(sql: &str, opts: &PlannerOptions) -> Result<LogicalPlan, OxiSqlError> {
226    let stmt = parse_one(sql)?;
227    plan_statement_with_opts(&stmt, opts)
228}
229
230// ── Tests ───────────────────────────────────────────────────────────────────
231
232#[cfg(test)]
233mod tests;