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::{
25 ConstantFolding, JoinAlgoHint, JoinAlgorithmPass, LimitPushThrough, OptPass, Optimizer,
26 PredicatePushdown, ProjectionPruning,
27};
28// ── Aggregate helpers ─────────────────────────────────────────────────────────
29pub mod agg;
30pub use agg::{extract_aggregates, is_aggregate_expr, AggFunc, AggregateExpr};
31// ── DML / Validator / Cost / Builder modules ─────────────────────────────────
32pub mod dml;
33pub use dml::{plan_dml, DmlPlan};
34pub mod validate;
35pub use validate::{SchemaValidator, ValidationError};
36pub mod cost;
37pub use cost::{CostEstimate, CostModel, TableStats};
38pub mod builder;
39mod columns;
40mod setops;
41mod window;
42pub use builder::QueryBuilder;
43
44// ── Plan types ───────────────────────────────────────────────────────────────
45pub mod plan;
46pub use plan::{JoinType, LogicalPlan, SetOpType, SortExpr, WindowFunctionDef};
47
48// ── Planner ──────────────────────────────────────────────────────────────────
49pub mod planner;
50pub use planner::{plan_query, plan_statement};
51
52// ── Plan explanation ─────────────────────────────────────────────────────────
53pub mod explain;
54pub use explain::explain;
55
56// ── Analysis utilities ────────────────────────────────────────────────────────
57pub mod analysis;
58pub use analysis::{count_params, extract_columns, extract_tables, is_read_only, normalize};
59
60// ── LRU parse cache ──────────────────────────────────────────────────────────
61pub mod cache;
62pub use cache::ParseCache;
63
64// ── Re-exports ──────────────────────────────────────────────────────────────
65use oxisql_core::OxiSqlError;
66pub use sqlparser::ast::{Expr, JoinConstraint, ObjectName, SelectItem, Statement, TableFactor};
67use sqlparser::dialect::Dialect;
68pub use sqlparser::dialect::{GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
69use sqlparser::parser::Parser;
70
71// ── SQL Dialect enum ────────────────────────────────────────────────────────
72/// SQL dialect selection for parsing.
73///
74/// Different databases have dialect-specific syntax; selecting the correct
75/// dialect improves parse accuracy for backend-specific SQL.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum SqlDialect {
78 /// ANSI / generic SQL dialect.
79 Generic,
80 /// PostgreSQL dialect (supports `::` casts, `$1` params, etc.).
81 Postgres,
82 /// MySQL dialect (supports backtick identifiers, etc.).
83 MySQL,
84 /// SQLite dialect (supports `AUTOINCREMENT`, backtick/bracket identifiers, etc.).
85 SQLite,
86}
87
88impl SqlDialect {
89 /// Return the `sqlparser` [`Dialect`] implementation for this variant.
90 pub(crate) fn to_sqlparser(self) -> Box<dyn Dialect> {
91 match self {
92 SqlDialect::Generic => Box::new(GenericDialect {}),
93 SqlDialect::Postgres => Box::new(PostgreSqlDialect {}),
94 SqlDialect::MySQL => Box::new(MySqlDialect {}),
95 SqlDialect::SQLite => Box::new(SQLiteDialect {}),
96 }
97 }
98}
99
100// ── Optimizer convenience wrapper ────────────────────────────────────────────
101
102/// Apply the default four-pass optimizer to `plan` and return the result.
103///
104/// This is a convenience shorthand for `Optimizer::new().optimize(plan)`.
105/// The passes run are: predicate pushdown, projection pruning (no-op when no
106/// required columns are specified), constant folding, and limit push-through.
107pub fn optimize(plan: LogicalPlan) -> LogicalPlan {
108 optimizer::optimize(plan)
109}
110
111// ── Dialect convenience wrappers ─────────────────────────────────────────────
112
113/// Parse SQL using the PostgreSQL dialect.
114///
115/// # Errors
116///
117/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
118pub fn parse_postgres(sql: &str) -> Result<Vec<Statement>, OxiSqlError> {
119 parse_with_dialect(sql, SqlDialect::Postgres)
120}
121
122/// Parse SQL using the MySQL dialect.
123///
124/// # Errors
125///
126/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
127pub fn parse_mysql(sql: &str) -> Result<Vec<Statement>, OxiSqlError> {
128 parse_with_dialect(sql, SqlDialect::MySQL)
129}
130
131/// Parse SQL using the SQLite dialect.
132///
133/// # Errors
134///
135/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
136pub fn parse_sqlite(sql: &str) -> Result<Vec<Statement>, OxiSqlError> {
137 parse_with_dialect(sql, SqlDialect::SQLite)
138}
139
140// ── Parsing functions ───────────────────────────────────────────────────────
141
142/// Parse a SQL string into a list of [`Statement`]s using the generic SQL
143/// dialect.
144///
145/// # Errors
146///
147/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
148pub fn parse(sql: &str) -> Result<Vec<Statement>, OxiSqlError> {
149 let dialect = GenericDialect {};
150 Parser::parse_sql(&dialect, sql).map_err(|e| OxiSqlError::Parse(e.to_string()))
151}
152
153/// Parse a SQL string using a specific [`SqlDialect`].
154///
155/// # Errors
156///
157/// Returns [`OxiSqlError::Parse`] if the string cannot be parsed.
158pub fn parse_with_dialect(sql: &str, dialect: SqlDialect) -> Result<Vec<Statement>, OxiSqlError> {
159 let d = dialect.to_sqlparser();
160 Parser::parse_sql(d.as_ref(), sql).map_err(|e| OxiSqlError::Parse(e.to_string()))
161}
162
163/// Parse a SQL string that is expected to contain exactly one statement.
164///
165/// Returns [`OxiSqlError::Parse`] if parsing fails or if the string contains
166/// zero or more than one statement.
167pub fn parse_one(sql: &str) -> Result<Statement, OxiSqlError> {
168 let mut stmts = parse(sql)?;
169 if stmts.len() != 1 {
170 return Err(OxiSqlError::Parse(format!(
171 "expected exactly 1 statement, got {}",
172 stmts.len()
173 )));
174 }
175 Ok(stmts.remove(0))
176}
177
178/// Parse a single statement using a specific dialect.
179pub fn parse_one_with_dialect(sql: &str, dialect: SqlDialect) -> Result<Statement, OxiSqlError> {
180 let mut stmts = parse_with_dialect(sql, dialect)?;
181 if stmts.len() != 1 {
182 return Err(OxiSqlError::Parse(format!(
183 "expected exactly 1 statement, got {}",
184 stmts.len()
185 )));
186 }
187 Ok(stmts.remove(0))
188}
189
190// ── Formatting ──────────────────────────────────────────────────────────────
191
192/// Convert a parsed AST statement back to a SQL string.
193///
194/// Uses sqlparser's `Display` implementation for round-trip formatting.
195pub fn format(stmt: &Statement) -> String {
196 stmt.to_string()
197}
198
199// ── Tests ───────────────────────────────────────────────────────────────────
200
201#[cfg(test)]
202mod tests;