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, TimeUnit, UnaryOperator, Variable,
93    When,
94};
95pub use insert_replace::{
96    InsertReplace, InsertReplaceFlag, InsertReplaceOnDuplicateKeyUpdate, InsertReplaceSet,
97    InsertReplaceSetPair, InsertReplaceType, OnConflict, OnConflictAction, OnConflictTarget,
98};
99pub use rename::{RenameTable, TableToTable};
100pub use select::{
101    IndexHint, IndexHintFor, IndexHintType, IndexHintUse, JoinSpecification, JoinType, Select,
102    SelectExpr, SelectFlag, TableReference,
103};
104pub use truncate::TruncateTable;
105pub use update::{Update, UpdateFlag};
106pub use with_query::{WithBlock, WithQuery};
107
108/// What sql diarect to parse as
109#[derive(Clone, Debug)]
110pub enum SQLDialect {
111    /// Parse MariaDB/Mysql SQL
112    MariaDB,
113    PostgreSQL,
114    Sqlite,
115}
116
117impl SQLDialect {
118    pub fn is_postgresql(&self) -> bool {
119        matches!(self, SQLDialect::PostgreSQL)
120    }
121
122    pub fn is_maria(&self) -> bool {
123        matches!(self, SQLDialect::MariaDB)
124    }
125
126    pub fn is_sqlite(&self) -> bool {
127        matches!(self, SQLDialect::Sqlite)
128    }
129}
130
131/// What kinds or arguments
132#[derive(Clone, Debug)]
133pub enum SQLArguments {
134    /// The statements do not contain arguments
135    None,
136    /// Arguments are %s or %d
137    Percent,
138    /// Arguments are ?
139    QuestionMark,
140    /// Arguments ar #i
141    Dollar,
142}
143
144/// Options used when parsing sql
145#[derive(Clone, Debug)]
146pub struct ParseOptions {
147    dialect: SQLDialect,
148    arguments: SQLArguments,
149    warn_unquoted_identifiers: bool,
150    warn_none_capital_keywords: bool,
151    list_hack: bool,
152}
153
154impl Default for ParseOptions {
155    fn default() -> Self {
156        Self {
157            dialect: SQLDialect::MariaDB,
158            arguments: SQLArguments::None,
159            warn_none_capital_keywords: false,
160            warn_unquoted_identifiers: false,
161            list_hack: false,
162        }
163    }
164}
165
166impl ParseOptions {
167    pub fn new() -> Self {
168        Default::default()
169    }
170
171    /// Change whan SQL dialect to use
172    pub fn dialect(self, dialect: SQLDialect) -> Self {
173        Self { dialect, ..self }
174    }
175
176    pub fn get_dialect(&self) -> SQLDialect {
177        self.dialect.clone()
178    }
179
180    /// Change what kinds of arguments are supplied
181    pub fn arguments(self, arguments: SQLArguments) -> Self {
182        Self { arguments, ..self }
183    }
184
185    /// Should we warn about unquoted identifiers
186    pub fn warn_unquoted_identifiers(self, warn_unquoted_identifiers: bool) -> Self {
187        Self {
188            warn_unquoted_identifiers,
189            ..self
190        }
191    }
192
193    /// Should we warn about unquoted identifiers
194    pub fn warn_none_capital_keywords(self, warn_none_capital_keywords: bool) -> Self {
195        Self {
196            warn_none_capital_keywords,
197            ..self
198        }
199    }
200
201    /// Parse _LIST_ as special expression
202    pub fn list_hack(self, list_hack: bool) -> Self {
203        Self { list_hack, ..self }
204    }
205}
206
207/// Construct an "Internal compiler error" issue, containing the current file and line
208#[macro_export]
209macro_rules! issue_ice {
210    ( $issues: expr, $spanned:expr ) => {{
211        $issues.err(
212            alloc::format!("Internal compiler error in {}:{}", file!(), line!()),
213            $spanned,
214        );
215    }};
216}
217
218/// Construct an "Not yet implemented" issue, containing the current file and line
219#[macro_export]
220macro_rules! issue_todo {
221    ( $issues: expr, $spanned:expr ) => {{
222        $issues.err(
223            alloc::format!("Not yet implemented {}:{}", file!(), line!()),
224            $spanned,
225        );
226    }};
227}
228
229/// Parse multiple statements,
230/// return an Vec of Statements even if there are parse errors.
231/// The statements are free of errors if no Error issues are
232/// added to issues
233pub fn parse_statements<'a>(
234    src: &'a str,
235    issues: &mut Issues<'a>,
236    options: &ParseOptions,
237) -> Vec<Statement<'a>> {
238    let mut parser = Parser::new(src, issues, options);
239    statement::parse_statements(&mut parser)
240}
241
242/// Parse a single statement,
243/// A statement may be returned even if there where parse errors.
244/// The statement is free of errors if no Error issues are
245/// added to issues
246pub fn parse_statement<'a>(
247    src: &'a str,
248    issues: &mut Issues<'a>,
249    options: &ParseOptions,
250) -> Option<Statement<'a>> {
251    let mut parser = Parser::new(src, issues, options);
252    match statement::parse_statement(&mut parser) {
253        Ok(Some(v)) => {
254            if parser.token != Token::Eof {
255                parser.expected_error("Unexpected token after statement")
256            }
257            Some(v)
258        }
259        Ok(None) => {
260            parser.expected_error("Statement");
261            None
262        }
263        Err(_) => None,
264    }
265}
266
267#[test]
268pub fn test_parse_alter_sql() {
269    let sql = "ALTER TABLE `test` ADD COLUMN `test1` VARCHAR (128) NULL DEFAULT NULL";
270    let options = ParseOptions::new()
271        .dialect(SQLDialect::MariaDB)
272        .arguments(SQLArguments::QuestionMark)
273        .warn_unquoted_identifiers(false);
274
275    let mut issues = Issues::new(sql);
276    parse_statement(sql, &mut issues, &options);
277    assert!(issues.is_ok(), "{}", issues);
278}
279
280#[test]
281pub fn test_parse_delete_sql_with_schema() {
282    let sql = "DROP TABLE IF EXISTS `test_schema`.`test`";
283    let options = ParseOptions::new()
284        .dialect(SQLDialect::MariaDB)
285        .arguments(SQLArguments::QuestionMark)
286        .warn_unquoted_identifiers(false);
287
288    let mut issues = Issues::new(sql);
289    parse_statement(sql, &mut issues, &options);
290    assert!(issues.is_ok(), "{}", issues);
291}
292
293#[test]
294pub fn parse_create_index_sql_with_schema() {
295    let sql = "CREATE INDEX `idx_test` ON  test_schema.test(`col_test`)";
296    let options = ParseOptions::new()
297        .dialect(SQLDialect::MariaDB)
298        .arguments(SQLArguments::QuestionMark)
299        .warn_unquoted_identifiers(false);
300
301    let mut issues = Issues::new(sql);
302    parse_statement(sql, &mut issues, &options);
303    assert!(issues.is_ok(), "{}", issues);
304}
305
306#[test]
307pub fn parse_create_index_sql_with_opclass() {
308    let sql = "CREATE INDEX idx_test ON test(path text_pattern_ops)";
309    let options = ParseOptions::new()
310        .dialect(SQLDialect::PostgreSQL)
311        .arguments(SQLArguments::Dollar)
312        .warn_unquoted_identifiers(false);
313
314    let mut issues = Issues::new(sql);
315    parse_statement(sql, &mut issues, &options);
316    assert!(issues.is_ok(), "{}", issues);
317}
318
319#[test]
320pub fn parse_drop_index_sql_with_schema() {
321    let sql = "DROP INDEX `idx_test` ON  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_create_view_sql_with_schema() {
335    let sql =
336        "CREATE OR REPLACE VIEW `test_schema`.`view_test` AS SELECT * FROM `test_schema`.`test`";
337    let options = ParseOptions::new()
338        .dialect(SQLDialect::MariaDB)
339        .arguments(SQLArguments::QuestionMark)
340        .warn_unquoted_identifiers(false);
341
342    let mut issues = Issues::new(sql);
343    let _result = parse_statement(sql, &mut issues, &options);
344    // assert!(result.is_none(), "result: {:#?}", &result);
345    assert!(issues.is_ok(), "{}", issues);
346}
347
348#[test]
349pub fn parse_drop_view_sql_with_schema() {
350    let sql = "DROP VIEW `test_schema`.`view_test`";
351    let options = ParseOptions::new()
352        .dialect(SQLDialect::MariaDB)
353        .arguments(SQLArguments::QuestionMark)
354        .warn_unquoted_identifiers(false);
355
356    let mut issues = Issues::new(sql);
357    let _result = parse_statement(sql, &mut issues, &options);
358    // assert!(result.is_none(), "result: {:#?}", &result);
359    assert!(issues.is_ok(), "{}", issues);
360}
361
362#[test]
363pub fn parse_truncate_table_sql_with_schema() {
364    let sql = "TRUNCATE TABLE `test_schema`.`table_test`";
365    let options = ParseOptions::new()
366        .dialect(SQLDialect::MariaDB)
367        .arguments(SQLArguments::QuestionMark)
368        .warn_unquoted_identifiers(false);
369
370    let mut issues = Issues::new(sql);
371    let _result = parse_statement(sql, &mut issues, &options);
372    // assert!(result.is_none(), "result: {:#?}", &result);
373    assert!(issues.is_ok(), "{}", issues);
374}
375
376#[test]
377pub fn parse_rename_table_sql_with_schema() {
378    let sql = "RENAME TABLE `test_schema`.`table_test` To `test_schema`.`table_new_test`";
379    let options = ParseOptions::new()
380        .dialect(SQLDialect::MariaDB)
381        .arguments(SQLArguments::QuestionMark)
382        .warn_unquoted_identifiers(false);
383
384    let mut issues = Issues::new(sql);
385    let _result = parse_statement(sql, &mut issues, &options);
386    // assert!(result.is_none(), "result: {:#?}", &result);
387    assert!(issues.is_ok(), "{}", issues);
388}
389
390#[test]
391pub fn parse_with_statement() {
392    let sql = "
393        WITH monkeys AS (DELETE FROM thing RETURNING id),
394        baz AS (SELECT id FROM cats WHERE comp IN (monkeys))
395        DELETE FROM dogs WHERE cat IN (cats)";
396    let options = ParseOptions::new()
397        .dialect(SQLDialect::PostgreSQL)
398        .arguments(SQLArguments::QuestionMark)
399        .warn_unquoted_identifiers(false);
400
401    let mut issues = Issues::new(sql);
402    let _result = parse_statement(sql, &mut issues, &options);
403    // assert!(result.is_none(), "result: {:#?}", &result);
404    assert!(issues.is_ok(), "{}", issues);
405}
406
407#[test]
408pub fn parse_use_index() {
409    let sql = "SELECT `a` FROM `b` FORCE INDEX FOR GROUP BY (`b`, `c`)";
410    let options = ParseOptions::new()
411        .dialect(SQLDialect::MariaDB)
412        .arguments(SQLArguments::QuestionMark)
413        .warn_unquoted_identifiers(false);
414
415    let mut issues = Issues::new(sql);
416    let _result = parse_statement(sql, &mut issues, &options);
417    assert!(issues.is_ok(), "{}", issues);
418}