sql_parse/
lib.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5// http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12
13//! Parse SQL into an AST
14//!
15//! This crate provides an lexer and parser that can parse SQL
16//! into an Abstract Syntax Tree (AST). Currently primarily focused
17//! on MariaDB/Mysql.
18//!
19//! Example code:
20//!
21//! ```
22//! use sql_parse::{SQLDialect, SQLArguments, ParseOptions, parse_statement, Issues};
23//!
24//! let options = ParseOptions::new()
25//!     .dialect(SQLDialect::MariaDB)
26//!     .arguments(SQLArguments::QuestionMark)
27//!     .warn_unquoted_identifiers(true);
28//!
29//!
30//! let sql = "SELECT `monkey`,
31//!            FROM `t1` LEFT JOIN `t2` ON `t2`.`id` = `t1.two`
32//!            WHERE `t1`.`id` = ?";
33//! let mut issues = Issues::new(sql);
34//! let ast = parse_statement(sql, &mut issues, &options);
35//!
36//! println!("{}", issues);
37//! println!("AST: {:#?}", ast);
38//! ```
39//!
40
41#![no_std]
42#![forbid(unsafe_code)]
43extern crate alloc;
44
45use alloc::vec::Vec;
46use lexer::Token;
47use parser::Parser;
48mod alter;
49mod create;
50mod data_type;
51mod delete;
52mod drop;
53mod expression;
54mod identifier;
55mod insert_replace;
56mod issue;
57mod keywords;
58mod lexer;
59mod parser;
60mod qualified_name;
61mod rename;
62mod select;
63mod span;
64mod sstring;
65mod statement;
66mod truncate;
67mod update;
68mod with_query;
69
70pub use data_type::{DataType, DataTypeProperty, Type};
71pub use identifier::Identifier;
72pub use issue::{Fragment, Issue, IssueHandle, Issues, Level};
73pub use qualified_name::QualifiedName;
74pub use span::{OptSpanned, Span, Spanned};
75pub use sstring::SString;
76pub use statement::{Statement, Union, UnionType, UnionWith};
77
78pub use alter::{
79    AlterColumnAction, AlterSpecification, AlterTable, ForeignKeyOn, ForeignKeyOnAction,
80    ForeignKeyOnType, IndexCol, IndexOption, IndexType,
81};
82pub use create::{
83    CreateAlgorithm, CreateDefinition, CreateFunction, CreateOption, CreateTable, CreateTrigger,
84    CreateView, TableOption,
85};
86pub use delete::{Delete, DeleteFlag};
87pub use drop::{
88    DropDatabase, DropEvent, DropFunction, DropIndex, DropProcedure, DropServer, DropTable,
89    DropTrigger, DropView,
90};
91pub use expression::{
92    BinaryOperator, Expression, Function, IdentifierPart, Is, UnaryOperator, Variable, When,
93};
94pub use insert_replace::{
95    InsertReplace, InsertReplaceFlag, InsertReplaceOnDuplicateKeyUpdate, InsertReplaceSet,
96    InsertReplaceSetPair, InsertReplaceType, OnConflict, OnConflictAction, OnConflictTarget,
97};
98pub use rename::{RenameTable, TableToTable};
99pub use select::{
100    IndexHint, IndexHintFor, IndexHintType, IndexHintUse, JoinSpecification, JoinType, Select,
101    SelectExpr, SelectFlag, TableReference,
102};
103pub use truncate::TruncateTable;
104pub use update::{Update, UpdateFlag};
105pub use with_query::{WithBlock, WithQuery};
106
107/// What sql diarect to parse as
108#[derive(Clone, Debug)]
109pub enum SQLDialect {
110    /// Parse MariaDB/Mysql SQL
111    MariaDB,
112    PostgreSQL,
113    Sqlite,
114}
115
116impl SQLDialect {
117    pub fn is_postgresql(&self) -> bool {
118        matches!(self, SQLDialect::PostgreSQL)
119    }
120
121    pub fn is_maria(&self) -> bool {
122        matches!(self, SQLDialect::MariaDB)
123    }
124
125    pub fn is_sqlite(&self) -> bool {
126        matches!(self, SQLDialect::Sqlite)
127    }
128}
129
130/// What kinds or arguments
131#[derive(Clone, Debug)]
132pub enum SQLArguments {
133    /// The statements do not contain arguments
134    None,
135    /// Arguments are %s or %d
136    Percent,
137    /// Arguments are ?
138    QuestionMark,
139    /// Arguments ar #i
140    Dollar,
141}
142
143/// Options used when parsing sql
144#[derive(Clone, Debug)]
145pub struct ParseOptions {
146    dialect: SQLDialect,
147    arguments: SQLArguments,
148    warn_unquoted_identifiers: bool,
149    warn_none_capital_keywords: bool,
150    list_hack: bool,
151}
152
153impl Default for ParseOptions {
154    fn default() -> Self {
155        Self {
156            dialect: SQLDialect::MariaDB,
157            arguments: SQLArguments::None,
158            warn_none_capital_keywords: false,
159            warn_unquoted_identifiers: false,
160            list_hack: false,
161        }
162    }
163}
164
165impl ParseOptions {
166    pub fn new() -> Self {
167        Default::default()
168    }
169
170    /// Change whan SQL dialect to use
171    pub fn dialect(self, dialect: SQLDialect) -> Self {
172        Self { dialect, ..self }
173    }
174
175    pub fn get_dialect(&self) -> SQLDialect {
176        self.dialect.clone()
177    }
178
179    /// Change what kinds of arguments are supplied
180    pub fn arguments(self, arguments: SQLArguments) -> Self {
181        Self { arguments, ..self }
182    }
183
184    /// Should we warn about unquoted identifiers
185    pub fn warn_unquoted_identifiers(self, warn_unquoted_identifiers: bool) -> Self {
186        Self {
187            warn_unquoted_identifiers,
188            ..self
189        }
190    }
191
192    /// Should we warn about unquoted identifiers
193    pub fn warn_none_capital_keywords(self, warn_none_capital_keywords: bool) -> Self {
194        Self {
195            warn_none_capital_keywords,
196            ..self
197        }
198    }
199
200    /// Parse _LIST_ as special expression
201    pub fn list_hack(self, list_hack: bool) -> Self {
202        Self { list_hack, ..self }
203    }
204}
205
206/// Construct an "Internal compiler error" issue, containing the current file and line
207#[macro_export]
208macro_rules! issue_ice {
209    ( $issues: expr, $spanned:expr ) => {{
210        $issues.err(
211            alloc::format!("Internal compiler error in {}:{}", file!(), line!()),
212            $spanned,
213        );
214    }};
215}
216
217/// Construct an "Not yet implemented" issue, containing the current file and line
218#[macro_export]
219macro_rules! issue_todo {
220    ( $issues: expr, $spanned:expr ) => {{
221        $issues.err(
222            alloc::format!("Not yet implemented {}:{}", file!(), line!()),
223            $spanned,
224        );
225    }};
226}
227
228/// Parse multiple statements,
229/// return an Vec of Statements even if there are parse errors.
230/// The statements are free of errors if no Error issues are
231/// added to issues
232pub fn parse_statements<'a>(
233    src: &'a str,
234    issues: &mut Issues<'a>,
235    options: &ParseOptions,
236) -> Vec<Statement<'a>> {
237    let mut parser = Parser::new(src, issues, options);
238    statement::parse_statements(&mut parser)
239}
240
241/// Parse a single statement,
242/// A statement may be returned even if there where parse errors.
243/// The statement is free of errors if no Error issues are
244/// added to issues
245pub fn parse_statement<'a>(
246    src: &'a str,
247    issues: &mut Issues<'a>,
248    options: &ParseOptions,
249) -> Option<Statement<'a>> {
250    let mut parser = Parser::new(src, issues, options);
251    match statement::parse_statement(&mut parser) {
252        Ok(Some(v)) => {
253            if parser.token != Token::Eof {
254                parser.expected_error("Unexpected token after statement")
255            }
256            Some(v)
257        }
258        Ok(None) => {
259            parser.expected_error("Statement");
260            None
261        }
262        Err(_) => None,
263    }
264}
265
266#[test]
267pub fn test_parse_alter_sql() {
268    let sql = "ALTER TABLE `test` ADD COLUMN `test1` VARCHAR (128) NULL DEFAULT NULL";
269    let options = ParseOptions::new()
270        .dialect(SQLDialect::MariaDB)
271        .arguments(SQLArguments::QuestionMark)
272        .warn_unquoted_identifiers(false);
273
274    let mut issues = Issues::new(sql);
275    parse_statement(sql, &mut issues, &options);
276    assert!(issues.is_ok(), "{}", issues);
277}
278
279#[test]
280pub fn test_parse_delete_sql_with_schema() {
281    let sql = "DROP TABLE IF EXISTS `test_schema`.`test`";
282    let options = ParseOptions::new()
283        .dialect(SQLDialect::MariaDB)
284        .arguments(SQLArguments::QuestionMark)
285        .warn_unquoted_identifiers(false);
286
287    let mut issues = Issues::new(sql);
288    parse_statement(sql, &mut issues, &options);
289    assert!(issues.is_ok(), "{}", issues);
290}
291#[test]
292pub fn parse_create_index_sql_with_schema() {
293    let sql = "CREATE INDEX `idx_test` ON  test_schema.test(`col_test`)";
294    let options = ParseOptions::new()
295        .dialect(SQLDialect::MariaDB)
296        .arguments(SQLArguments::QuestionMark)
297        .warn_unquoted_identifiers(false);
298
299    let mut issues = Issues::new(sql);
300    parse_statement(sql, &mut issues, &options);
301    assert!(issues.is_ok(), "{}", issues);
302}
303
304#[test]
305pub fn parse_drop_index_sql_with_schema() {
306    let sql = "DROP INDEX `idx_test` ON  test_schema.test";
307    let options = ParseOptions::new()
308        .dialect(SQLDialect::MariaDB)
309        .arguments(SQLArguments::QuestionMark)
310        .warn_unquoted_identifiers(false);
311
312    let mut issues = Issues::new(sql);
313    let _result = parse_statement(sql, &mut issues, &options);
314    // assert!(result.is_none(), "result: {:#?}", &result);
315    assert!(issues.is_ok(), "{}", issues);
316}
317
318#[test]
319pub fn parse_create_view_sql_with_schema() {
320    let sql =
321        "CREATE OR REPLACE VIEW `test_schema`.`view_test` AS SELECT * FROM `test_schema`.`test`";
322    let options = ParseOptions::new()
323        .dialect(SQLDialect::MariaDB)
324        .arguments(SQLArguments::QuestionMark)
325        .warn_unquoted_identifiers(false);
326
327    let mut issues = Issues::new(sql);
328    let _result = parse_statement(sql, &mut issues, &options);
329    // assert!(result.is_none(), "result: {:#?}", &result);
330    assert!(issues.is_ok(), "{}", issues);
331}
332
333#[test]
334pub fn parse_drop_view_sql_with_schema() {
335    let sql = "DROP VIEW `test_schema`.`view_test`";
336    let options = ParseOptions::new()
337        .dialect(SQLDialect::MariaDB)
338        .arguments(SQLArguments::QuestionMark)
339        .warn_unquoted_identifiers(false);
340
341    let mut issues = Issues::new(sql);
342    let _result = parse_statement(sql, &mut issues, &options);
343    // assert!(result.is_none(), "result: {:#?}", &result);
344    assert!(issues.is_ok(), "{}", issues);
345}
346
347#[test]
348pub fn parse_truncate_table_sql_with_schema() {
349    let sql = "TRUNCATE TABLE `test_schema`.`table_test`";
350    let options = ParseOptions::new()
351        .dialect(SQLDialect::MariaDB)
352        .arguments(SQLArguments::QuestionMark)
353        .warn_unquoted_identifiers(false);
354
355    let mut issues = Issues::new(sql);
356    let _result = parse_statement(sql, &mut issues, &options);
357    // assert!(result.is_none(), "result: {:#?}", &result);
358    assert!(issues.is_ok(), "{}", issues);
359}
360
361#[test]
362pub fn parse_rename_table_sql_with_schema() {
363    let sql = "RENAME TABLE `test_schema`.`table_test` To `test_schema`.`table_new_test`";
364    let options = ParseOptions::new()
365        .dialect(SQLDialect::MariaDB)
366        .arguments(SQLArguments::QuestionMark)
367        .warn_unquoted_identifiers(false);
368
369    let mut issues = Issues::new(sql);
370    let _result = parse_statement(sql, &mut issues, &options);
371    // assert!(result.is_none(), "result: {:#?}", &result);
372    assert!(issues.is_ok(), "{}", issues);
373}
374
375#[test]
376pub fn parse_with_statement() {
377    let sql = "
378        WITH monkeys AS (DELETE FROM thing RETURNING id),
379        baz AS (SELECT id FROM cats WHERE comp IN (monkeys))
380        DELETE FROM dogs WHERE cat IN (cats)";
381    let options = ParseOptions::new()
382        .dialect(SQLDialect::PostgreSQL)
383        .arguments(SQLArguments::QuestionMark)
384        .warn_unquoted_identifiers(false);
385
386    let mut issues = Issues::new(sql);
387    let _result = parse_statement(sql, &mut issues, &options);
388    // assert!(result.is_none(), "result: {:#?}", &result);
389    assert!(issues.is_ok(), "{}", issues);
390}
391
392#[test]
393pub fn parse_use_index() {
394    let sql = "SELECT `a` FROM `b` FORCE INDEX FOR GROUP BY (`b`, `c`)";
395    let options = ParseOptions::new()
396        .dialect(SQLDialect::MariaDB)
397        .arguments(SQLArguments::QuestionMark)
398        .warn_unquoted_identifiers(false);
399
400    let mut issues = Issues::new(sql);
401    let _result = parse_statement(sql, &mut issues, &options);
402    assert!(issues.is_ok(), "{}", issues);
403}