diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..a73d1dc4e9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: cargo + directory: "/sqlparser_bench" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 06db11ebfa..078d8bae03 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,30 +1,78 @@ name: Rust -on: [push] +on: [push, pull_request] jobs: - build: + + codestyle: + runs-on: ubuntu-latest + steps: + - name: Set up Rust + uses: hecrj/setup-rust-action@v1 + with: + components: rustfmt + # Note that `nightly` is required for `license_template_path`, as + # it's an unstable feature. + rust-version: nightly + - uses: actions/checkout@v2 + - run: cargo +nightly fmt -- --check --config-path <(echo 'license_template_path = "HEADER"') + + lint: + runs-on: ubuntu-latest + steps: + - name: Set up Rust + uses: hecrj/setup-rust-action@v1 + with: + components: clippy + - uses: actions/checkout@v2 + - run: cargo clippy --all-targets --all-features -- -D warnings + + compile: runs-on: ubuntu-latest + steps: + - name: Set up Rust + uses: hecrj/setup-rust-action@v1 + - uses: actions/checkout@master + - run: cargo check --all-targets --all-features + test: + strategy: + matrix: + rust: [stable, beta, nightly] + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - name: Setup Rust - run: | - rustup toolchain install nightly --profile default - rustup toolchain install stable - rustup override set stable - # Clippy must be run first, as its lints are only triggered during - # compilation. Put another way: after a successful `cargo build`, `cargo - # clippy` is guaranteed to produce no results. This bug is known upstream: - # https://github.com/rust-lang/rust-clippy/issues/2604. -# - name: Clippy -# run: cargo clippy -- --all-targets --all-features -- -D warnings - - name: Check formatting - run: | - cargo +nightly fmt -- --check --config-path <(echo 'license_template_path = "HEADER"') - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose - - name: Run tests for all features - run: cargo test --verbose -- all-features + uses: hecrj/setup-rust-action@v1 + with: + rust-version: ${{ matrix.rust }} + - name: Install Tarpaulin + uses: actions-rs/install@v0.1 + with: + crate: cargo-tarpaulin + version: 0.14.2 + use-tool-cache: true + - name: Checkout + uses: actions/checkout@v2 + - name: Test + run: cargo test --all-features + - name: Coverage + if: matrix.rust == 'stable' + run: cargo tarpaulin -o Lcov --output-dir ./coverage + - name: Coveralls + if: matrix.rust == 'stable' + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + publish-crate: + if: startsWith(github.ref, 'refs/tags/v0') + runs-on: ubuntu-latest + needs: [test] + steps: + - name: Set up Rust + uses: hecrj/setup-rust-action@v1 + - uses: actions/checkout@v2 + - name: Publish + shell: bash + run: | + cargo publish --token ${{ secrets.CRATES_TOKEN }} diff --git a/.gitignore b/.gitignore index 46c66224f6..dcc3cbd939 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ +/sqlparser_bench/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c25da188..0701b23dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,74 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Given that the parser produces a typed AST, any changes to the AST will technically be breaking and thus will result in a `0.(N+1)` version. We document changes that break via addition as "Added". ## [Unreleased] -Check https://github.com/andygrove/sqlparser-rs/commits/master for undocumented changes. +Check https://github.com/ballista-compute/sqlparser-rs/commits/main for undocumented changes. + + +## [0.8.0] 2020-02-20 + +### Added +* Introduce Hive QL dialect `HiveDialect` and syntax (#235) - Thanks @hntd187! +* Add `SUBSTRING(col [FROM ] [FOR ])` syntax (#293) +* Support parsing floats without leading digits `.01` (#294) +* Support parsing multiple show variables (#290) - Thanks @francis-du! +* Support SQLite `INSERT OR [..]` syntax (#281) - Thanks @zhangli-pear! + +## [0.7.0] 2020-12-28 ### Changed +- Change the MySQL dialect to support `` `identifiers` `` quoted with backticks instead of the standard `"double-quoted"` identifiers (#247) - thanks @mashuai! +- Update bigdecimal requirement from 0.1 to 0.2 (#268) + +### Added +- Enable dialect-specific behaviours in the parser (`dialect_of!()`) (#254) - thanks @eyalleshem! +- Support named arguments in function invocations (`ARG_NAME => val`) (#250) - thanks @eyalleshem! +- Support `TABLE()` functions in `FROM` (#253) - thanks @eyalleshem! +- Support Snowflake's single-line comments starting with '#' or '//' (#264) - thanks @eyalleshem! +- Support PostgreSQL `PREPARE`, `EXECUTE`, and `DEALLOCATE` (#243) - thanks @silathdiir! +- Support PostgreSQL math operators (#267) - thanks @alex-dukhno! +- Add SQLite dialect (#248) - thanks @mashuai! +- Add Snowflake dialect (#259) - thanks @eyalleshem! +- Support for Recursive CTEs - thanks @rhanqtl! +- Support `FROM (table_name) alias` syntax - thanks @eyalleshem! +- Support for `EXPLAIN [ANALYZE] VERBOSE` - thanks @ovr! +- Support `ANALYZE TABLE` +- DDL: + - Support `OR REPLACE` in `CREATE VIEW`/`TABLE` (#239) - thanks @Dandandan! + - Support specifying `ASC`/`DESC` in index columns (#249) - thanks @mashuai! + - Support SQLite `AUTOINCREMENT` and MySQL `AUTO_INCREMENT` column option in `CREATE TABLE` (#234) - thanks @mashuai! + - Support PostgreSQL `IF NOT EXISTS` for `CREATE SCHEMA` (#276) - thanks @alex-dukhno! + +### Fixed +- Fix a typo in `JSONFILE` serialization, introduced in 0.3.1 (#237) +- Change `CREATE INDEX` serialization to not end with a semicolon, introduced in 0.5.1 (#245) +- Don't fail parsing `ALTER TABLE ADD COLUMN` ending with a semicolon, introduced in 0.5.1 (#246) - thanks @mashuai + +## [0.6.1] - 2020-07-20 + +### Added +- Support BigQuery `ASSERT` statement (#226) + +## [0.6.0] - 2020-07-20 + +### Added +- Support SQLite's `CREATE TABLE (...) WITHOUT ROWID` (#208) - thanks @mashuai! +- Support SQLite's `CREATE VIRTUAL TABLE` (#209) - thanks @mashuai! + +## [0.5.1] - 2020-06-26 +This release should have been called `0.6`, as it introduces multiple incompatible changes to the API. If you don't want to upgrade yet, you can revert to the previous version by changing your `Cargo.toml` to: + + sqlparser = "= 0.5.0" + + +### Changed +- **`Parser::parse_sql` now accepts a `&str` instead of `String` (#182)** - thanks @Dandandan! - Change `Ident` (previously a simple `String`) to store the parsed (unquoted) `value` of the identifier and the `quote_style` separately (#143) - thanks @apparebit! - Support Snowflake's `FROM (table_name)` (#155) - thanks @eyalleshem! +- Add line and column number to TokenizerError (#194) - thanks @Dandandan! +- Use Token::EOF instead of Option (#195) +- Make the units keyword following `INTERVAL '...'` optional (#184) - thanks @maxcountryman! +- Generalize `DATE`/`TIME`/`TIMESTAMP` literals representation in the AST (`TypedString { data_type, value }`) and allow `DATE` and other keywords to be used as identifiers when not followed by a string (#187) - thanks @maxcountryman! +- Output DataType capitalized (`fmt::Display`) (#202) - thanks @Dandandan! ### Added - Support MSSQL `TOP () [ PERCENT ] [ WITH TIES ]` (#150) - thanks @alexkyllo! @@ -21,9 +84,16 @@ Check https://github.com/andygrove/sqlparser-rs/commits/master for undocumented - Support basic forms of `CREATE SCHEMA` and `DROP SCHEMA` (#173) - thanks @alex-dukhno! - Support `NULLS FIRST`/`LAST` in `ORDER BY` expressions (#176) - thanks @houqp! - Support `LISTAGG()` (#174) - thanks @maxcountryman! +- Support the string concatentation operator `||` (#178) - thanks @Dandandan! +- Support bitwise AND (`&`), OR (`|`), XOR (`^`) (#181) - thanks @Dandandan! +- Add serde support to AST structs and enums (#196) - thanks @panarch! +- Support `ALTER TABLE ADD COLUMN`, `RENAME COLUMN`, and `RENAME TO` (#203) - thanks @mashuai! +- Support `ALTER TABLE DROP COLUMN` (#148) - thanks @ivanceras! +- Support `CREATE TABLE ... AS ...` (#206) - thanks @Dandandan! ### Fixed - Report an error for unterminated string literals (#165) +- Make file format (`STORED AS`) case insensitive (#200) and don't allow quoting it (#201) - thanks @Dandandan! ## [0.5.0] - 2019-10-10 @@ -138,3 +208,4 @@ We don't have a changelog for the changes made in 2018, but thanks to @crw5996, ## [0.1.0] - 2018-09-03 Initial release + diff --git a/Cargo.toml b/Cargo.toml index 430d2d3dcd..4cfe3209a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.5.1-alpha-0" +version = "0.8.1-alpha.0" authors = ["Andy Grove "] -homepage = "https://github.com/andygrove/sqlparser-rs" +homepage = "https://github.com/ballista-compute/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" keywords = [ "ansi", "sql", "lexer", "parser" ] -repository = "https://github.com/andygrove/sqlparser-rs" +repository = "https://github.com/ballista-compute/sqlparser-rs" license = "Apache-2.0" include = [ "src/**/*.rs", @@ -19,15 +19,25 @@ name = "sqlparser" path = "src/lib.rs" [features] -cst = ["rowan"] # Retain a concrete synatax tree, available as `parser.syntax()` +cst = ["rowan"] # Retain a concrete synatax tree, available as `parser.syntax()` +json_example = ["serde_json", "serde"] # Enable JSON output in the `cli` example: [dependencies] -bigdecimal = { version = "0.1.0", optional = true, features = ["serde"] } -log = "0.4.5" +bigdecimal = { version = "0.2", features = ["serde"], optional = true } +log = "0.4" rowan = { version = "0.10.0", optional = true, features = ["serde1"] } -serde = { version = "1.0.106", features = ["derive"] } -serde_json = "1.0.52" +serde = { version = "1.0", features = ["derive"], optional = true } +# serde_json is only used in examples/cli, but we have to put it outside +# of dev-dependencies because of +# https://github.com/rust-lang/cargo/issues/1596 +serde_json = { version = "1.0", optional = true } [dev-dependencies] -simple_logger = "1.0.1" +simple_logger = "1.9" matches = "0.1" + +[package.metadata.release] +# Instruct `cargo release` to not run `cargo publish` locally: +# https://github.com/sunng87/cargo-release/blob/master/docs/reference.md#config-fields +# See docs/releasing.md for details. +disable-publish = true diff --git a/README.md b/README.md index 170a31dee2..7c4b43ab58 100644 --- a/README.md +++ b/README.md @@ -185,8 +185,8 @@ Remarks [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Version](https://img.shields.io/crates/v/sqlparser.svg)](https://crates.io/crates/sqlparser) -[![Build Status](https://travis-ci.org/andygrove/sqlparser-rs.svg?branch=master)](https://travis-ci.org/andygrove/sqlparser-rs) -[![Coverage Status](https://coveralls.io/repos/github/andygrove/sqlparser-rs/badge.svg?branch=master)](https://coveralls.io/github/andygrove/sqlparser-rs?branch=master) +[![Build Status](https://github.com/ballista-compute/sqlparser-rs/workflows/Rust/badge.svg?branch=main)](https://github.com/ballista-compute/sqlparser-rs/actions?query=workflow%3ARust+branch%3Amain) +[![Coverage Status](https://coveralls.io/repos/github/ballista-compute/sqlparser-rs/badge.svg?branch=main)](https://coveralls.io/github/ballista-compute/sqlparser-rs?branch=main) [![Gitter Chat](https://badges.gitter.im/sqlparser-rs/community.svg)](https://gitter.im/sqlparser-rs/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) The goal of this project is to build a SQL lexer and parser capable of parsing @@ -194,8 +194,8 @@ SQL that conforms with the [ANSI/ISO SQL standard][sql-standard] while also making it easy to support custom dialects so that this crate can be used as a foundation for vendor-specific parsers. -This parser is currently being used by the [DataFusion] query engine and -[LocustDB]. +This parser is currently being used by the [DataFusion] query engine, +[LocustDB], and [Ballista]. ## Example @@ -223,6 +223,12 @@ This outputs AST: [Query(Query { ctes: [], body: Select(Select { distinct: false, projection: [UnnamedExpr(Identifier("a")), UnnamedExpr(Identifier("b")), UnnamedExpr(Value(Long(123))), UnnamedExpr(Function(Function { name: ObjectName(["myfunc"]), args: [Identifier("b")], over: None, distinct: false }))], from: [TableWithJoins { relation: Table { name: ObjectName(["table_1"]), alias: None, args: [], with_hints: [] }, joins: [] }], selection: Some(BinaryOp { left: BinaryOp { left: Identifier("a"), op: Gt, right: Identifier("b") }, op: And, right: BinaryOp { left: Identifier("b"), op: Lt, right: Value(Long(100)) } }), group_by: [], having: None }), order_by: [OrderByExpr { expr: Identifier("a"), asc: Some(false) }, OrderByExpr { expr: Identifier("b"), asc: None }], limit: None, offset: None, fetch: None })] ``` +## Command line +To parse a file and dump the results as JSON: +``` +$ cargo run --features json_example --example cli FILENAME.sql [--dialectname] +``` + ## SQL compliance SQL was first standardized in 1987, and revisions of the standard have been @@ -300,9 +306,10 @@ resources. [tdop-tutorial]: https://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing [`cargo fmt`]: https://github.com/rust-lang/rustfmt#on-the-stable-toolchain -[current issues]: https://github.com/andygrove/sqlparser-rs/issues +[current issues]: https://github.com/ballista-compute/sqlparser-rs/issues [DataFusion]: https://github.com/apache/arrow/tree/master/rust/datafusion [LocustDB]: https://github.com/cswinter/LocustDB +[Ballista]: https://github.com/ballista-compute/ballista [Pratt Parser]: https://tdop.github.io/ [sql-2016-grammar]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html [sql-standard]: https://en.wikipedia.org/wiki/ISO/IEC_9075 diff --git a/docs/benchmarking.md b/docs/benchmarking.md new file mode 100644 index 0000000000..feae53c84b --- /dev/null +++ b/docs/benchmarking.md @@ -0,0 +1,6 @@ +# Benchmarking + +Run `cargo bench` in the project `sqlparser_bench` execute the queries. +It will report results using the `criterion` library to perform the benchmarking. + +The bench project lives in another crate, to avoid the negative impact on building the `sqlparser` crate. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000000..58509eca61 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,58 @@ +# Releasing + +## Prerequisites +Publishing to crates.io has been automated via GitHub Actions, so you will only +need push access to the [ballista-compute GitHub repository](https://github.com/ballista-compute/sqlparser-rs) +in order to publish a release. + +We use the [`cargo release`](https://github.com/sunng87/cargo-release) +subcommand to ensure correct versioning. Install via: + +``` +$ cargo install cargo-release +``` + +## Process + +1. **Before releasing** ensure `CHANGELOG.md` is updated appropriately and that + you have a clean checkout of the `main` branch of the sqlparser repository: + ``` + $ git fetch && git status + On branch main + Your branch is up to date with 'upstream/main'. + + nothing to commit, working tree clean + ``` + * If you have the time, check that the examples in the README are up to date. + +2. Using `cargo-release` we can publish a new release like so: + + ``` + $ cargo release minor --push-remote upstream + ``` + + You can add `--dry-run` to see what the command is going to do, + or `--skip-push` to stop before actually publishing the release. + + `cargo release` will then: + + * Bump the minor part of the version in `Cargo.toml` (e.g. `0.7.1-alpha.0` + -> `0.8.0`. You can use `patch` instead of `minor`, as appropriate). + * Create a new tag (e.g. `v0.8.0`) locally + * Push the new tag to the specified remote (`upstream` in the above + example), which will trigger a publishing process to crates.io as part of + the [corresponding GitHub Action](https://github.com/ballista-compute/sqlparser-rs/blob/main/.github/workflows/rust.yml). + + Note that credentials for authoring in this way are securely stored in + the (GitHub) repo secrets as `CRATE_TOKEN`. + * Bump the crate version again (to something like `0.8.1-alpha.0`) to + indicate the start of new development cycle. + +3. Push the updates to the `main` branch upstream: + ``` + $ git push upstream + ``` + +4. Check that the new version of the crate is available on crates.io: + https://crates.io/crates/sqlparser + diff --git a/examples/cli.rs b/examples/cli.rs index 936e0e132d..765178e197 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -16,21 +16,32 @@ /// Run with `cargo run --example cli` use std::fs; +use simple_logger::SimpleLogger; use sqlparser::dialect::*; use sqlparser::{parser::Parser, tokenizer::Tokenizer}; fn main() { - simple_logger::init().unwrap(); + SimpleLogger::new().init().unwrap(); let filename = std::env::args().nth(1).expect( - "No arguments provided!\n\n\ - Usage: cargo run --example cli FILENAME.sql [--dialectname]", + r#" +No arguments provided! + +Usage: +$ cargo run --example cli FILENAME.sql [--dialectname] + +To print the parse results as JSON: +$ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] + +"#, ); let dialect: Box = match std::env::args().nth(2).unwrap_or_default().as_ref() { "--ansi" => Box::new(AnsiDialect {}), "--postgres" => Box::new(PostgreSqlDialect {}), "--ms" => Box::new(MsSqlDialect {}), + "--snowflake" => Box::new(SnowflakeDialect {}), + "--hive" => Box::new(HiveDialect {}), "--generic" | "" => Box::new(GenericDialect {}), s => panic!("Unexpected parameter: {}", s), }; @@ -53,7 +64,7 @@ fn main() { std::process::exit(1); }); - let mut parser = Parser::new(tokens); + let mut parser = Parser::new(tokens, &*dialect); let parse_result = parser.parse_statements(); match parse_result { @@ -68,16 +79,14 @@ fn main() { .join("\n") ); println!("Parse results:\n{:#?}", statements); - } else { - #[cfg(feature = "cst")] + } else if cfg!(feature = "json_example") { + #[cfg(feature = "json_example")] { - let syn = parser.syntax(); - println!("Parse tree:\n{:#?}", syn); - let serialized = serde_json::to_string(&syn).unwrap(); - println!("Parse tree as json:\n{}", serialized); - let serialized = serde_json::to_string(&statements).unwrap(); - println!("AST as JSON:\n{}", serialized); + let serialized = serde_json::to_string_pretty(&statements).unwrap(); + println!("Serialized as JSON:\n{}", serialized); } + } else { + println!("Parse results:\n{:#?}", statements); } } Err(e) => { diff --git a/sqlparser_bench/Cargo.toml b/sqlparser_bench/Cargo.toml new file mode 100644 index 0000000000..d98ff156c6 --- /dev/null +++ b/sqlparser_bench/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sqlparser_bench" +version = "0.1.0" +authors = ["Dandandan "] +edition = "2018" + +[dependencies] +sqlparser = { path = "../" } + +[dev-dependencies] +criterion = "0.3" + +[[bench]] +name = "sqlparser_bench" +harness = false diff --git a/sqlparser_bench/benches/sqlparser_bench.rs b/sqlparser_bench/benches/sqlparser_bench.rs new file mode 100644 index 0000000000..5293c0f501 --- /dev/null +++ b/sqlparser_bench/benches/sqlparser_bench.rs @@ -0,0 +1,43 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use criterion::{criterion_group, criterion_main, Criterion}; +use sqlparser::dialect::GenericDialect; +use sqlparser::parser::Parser; + +fn basic_queries(c: &mut Criterion) { + let mut group = c.benchmark_group("sqlparser-rs parsing benchmark"); + let dialect = GenericDialect {}; + + let string = "SELECT * FROM table WHERE 1 = 1"; + group.bench_function("sqlparser::select", |b| { + b.iter(|| Parser::parse_sql(&dialect, string)); + }); + + let with_query = " + WITH derived AS ( + SELECT MAX(a) AS max_a, + COUNT(b) AS b_num, + user_id + FROM TABLE + GROUP BY user_id + ) + SELECT * FROM table + LEFT JOIN derived USING (user_id) + "; + group.bench_function("sqlparser::with_select", |b| { + b.iter(|| Parser::parse_sql(&dialect, with_query)); + }); +} + +criterion_group!(benches, basic_queries); +criterion_main!(benches); diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index d7911546f6..388703e769 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -11,11 +11,13 @@ // limitations under the License. use super::ObjectName; +#[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::fmt; /// SQL data types -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum DataType { /// Fixed-length character type e.g. CHAR(10) Char(Option), @@ -59,6 +61,8 @@ pub enum DataType { Regclass, /// Text Text, + /// String + String, /// Bytea Bytea, /// Custom type such as enums @@ -70,36 +74,37 @@ pub enum DataType { impl fmt::Display for DataType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - DataType::Char(size) => format_type_with_optional_length(f, "char", size), + DataType::Char(size) => format_type_with_optional_length(f, "CHAR", size), DataType::Varchar(size) => { - format_type_with_optional_length(f, "character varying", size) + format_type_with_optional_length(f, "CHARACTER VARYING", size) } - DataType::Uuid => write!(f, "uuid"), - DataType::Clob(size) => write!(f, "clob({})", size), - DataType::Binary(size) => write!(f, "binary({})", size), - DataType::Varbinary(size) => write!(f, "varbinary({})", size), - DataType::Blob(size) => write!(f, "blob({})", size), + DataType::Uuid => write!(f, "UUID"), + DataType::Clob(size) => write!(f, "CLOB({})", size), + DataType::Binary(size) => write!(f, "BINARY({})", size), + DataType::Varbinary(size) => write!(f, "VARBINARY({})", size), + DataType::Blob(size) => write!(f, "BLOB({})", size), DataType::Decimal(precision, scale) => { if let Some(scale) = scale { - write!(f, "numeric({},{})", precision.unwrap(), scale) + write!(f, "NUMERIC({},{})", precision.unwrap(), scale) } else { - format_type_with_optional_length(f, "numeric", precision) + format_type_with_optional_length(f, "NUMERIC", precision) } } - DataType::Float(size) => format_type_with_optional_length(f, "float", size), - DataType::SmallInt => write!(f, "smallint"), - DataType::Int => write!(f, "int"), - DataType::BigInt => write!(f, "bigint"), - DataType::Real => write!(f, "real"), - DataType::Double => write!(f, "double"), - DataType::Boolean => write!(f, "boolean"), - DataType::Date => write!(f, "date"), - DataType::Time => write!(f, "time"), - DataType::Timestamp => write!(f, "timestamp"), - DataType::Interval => write!(f, "interval"), - DataType::Regclass => write!(f, "regclass"), - DataType::Text => write!(f, "text"), - DataType::Bytea => write!(f, "bytea"), + DataType::Float(size) => format_type_with_optional_length(f, "FLOAT", size), + DataType::SmallInt => write!(f, "SMALLINT"), + DataType::Int => write!(f, "INT"), + DataType::BigInt => write!(f, "BIGINT"), + DataType::Real => write!(f, "REAL"), + DataType::Double => write!(f, "DOUBLE"), + DataType::Boolean => write!(f, "BOOLEAN"), + DataType::Date => write!(f, "DATE"), + DataType::Time => write!(f, "TIME"), + DataType::Timestamp => write!(f, "TIMESTAMP"), + DataType::Interval => write!(f, "INTERVAL"), + DataType::Regclass => write!(f, "REGCLASS"), + DataType::Text => write!(f, "TEXT"), + DataType::String => write!(f, "STRING"), + DataType::Bytea => write!(f, "BYTEA"), DataType::Array(ty) => write!(f, "{}[]", ty), DataType::Custom(ty) => write!(f, "{}", ty), } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index b9febffecc..67dc2e3227 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -13,30 +13,116 @@ //! AST types specific to CREATE/ALTER variants of [Statement] //! (commonly referred to as Data Definition Language, or DDL) use super::{display_comma_separated, DataType, Expr, Ident, ObjectName}; +use crate::ast::display_separated; +use crate::tokenizer::Token; +#[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::fmt; /// An `ALTER TABLE` (`Statement::AlterTable`) operation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum AlterTableOperation { /// `ADD ` AddConstraint(TableConstraint), + /// `ADD [ COLUMN ] ` + AddColumn { column_def: ColumnDef }, /// TODO: implement `DROP CONSTRAINT ` DropConstraint { name: Ident }, + /// `DROP [ COLUMN ] [ IF EXISTS ] [ CASCADE ]` + DropColumn { + column_name: Ident, + if_exists: bool, + cascade: bool, + }, + /// `RENAME TO PARTITION (partition=val)` + RenamePartitions { + old_partitions: Vec, + new_partitions: Vec, + }, + /// Add Partitions + AddPartitions { + if_not_exists: bool, + new_partitions: Vec, + }, + DropPartitions { + partitions: Vec, + if_exists: bool, + }, + /// `RENAME [ COLUMN ] TO ` + RenameColumn { + old_column_name: Ident, + new_column_name: Ident, + }, + /// `RENAME TO ` + RenameTable { table_name: ObjectName }, } impl fmt::Display for AlterTableOperation { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + AlterTableOperation::AddPartitions { + if_not_exists, + new_partitions, + } => write!( + f, + "ADD{ine} PARTITION ({})", + display_comma_separated(new_partitions), + ine = if *if_not_exists { " IF NOT EXISTS" } else { "" } + ), AlterTableOperation::AddConstraint(c) => write!(f, "ADD {}", c), + AlterTableOperation::AddColumn { column_def } => { + write!(f, "ADD COLUMN {}", column_def.to_string()) + } + AlterTableOperation::DropPartitions { + partitions, + if_exists, + } => write!( + f, + "DROP{ie} PARTITION ({})", + display_comma_separated(partitions), + ie = if *if_exists { " IF EXISTS" } else { "" } + ), AlterTableOperation::DropConstraint { name } => write!(f, "DROP CONSTRAINT {}", name), + AlterTableOperation::DropColumn { + column_name, + if_exists, + cascade, + } => write!( + f, + "DROP COLUMN {}{}{}", + if *if_exists { "IF EXISTS " } else { "" }, + column_name, + if *cascade { " CASCADE" } else { "" } + ), + AlterTableOperation::RenamePartitions { + old_partitions, + new_partitions, + } => write!( + f, + "PARTITION ({}) RENAME TO PARTITION ({})", + display_comma_separated(old_partitions), + display_comma_separated(new_partitions) + ), + AlterTableOperation::RenameColumn { + old_column_name, + new_column_name, + } => write!( + f, + "RENAME COLUMN {} TO {}", + old_column_name, new_column_name + ), + AlterTableOperation::RenameTable { table_name } => { + write!(f, "RENAME TO {}", table_name) + } } } } /// A table-level constraint, specified in a `CREATE TABLE` or an /// `ALTER TABLE ADD ` statement. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TableConstraint { /// `[ CONSTRAINT ] { PRIMARY KEY | UNIQUE } ()` Unique { @@ -95,7 +181,8 @@ impl fmt::Display for TableConstraint { } /// SQL column definition -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ColumnDef { pub name: Ident, pub data_type: DataType, @@ -129,7 +216,8 @@ impl fmt::Display for ColumnDef { /// For maximum flexibility, we don't distinguish between constraint and /// non-constraint options, lumping them all together under the umbrella of /// "column options," and we allow any column option to be named. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ColumnOptionDef { pub name: Option, pub option: ColumnOption, @@ -143,7 +231,8 @@ impl fmt::Display for ColumnOptionDef { /// `ColumnOption`s are modifiers that follow a column definition in a `CREATE /// TABLE` statement. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ColumnOption { /// `NULL` Null, @@ -152,9 +241,7 @@ pub enum ColumnOption { /// `DEFAULT ` Default(Expr), /// `{ PRIMARY KEY | UNIQUE }` - Unique { - is_primary: bool, - }, + Unique { is_primary: bool }, /// A referential integrity constraint (`[FOREIGN KEY REFERENCES /// () /// { [ON DELETE ] [ON UPDATE ] | @@ -166,8 +253,12 @@ pub enum ColumnOption { on_delete: Option, on_update: Option, }, - // `CHECK ()` + /// `CHECK ()` Check(Expr), + /// Dialect-specific options, such as: + /// - MySQL's `AUTO_INCREMENT` or SQLite's `AUTOINCREMENT` + /// - ... + DialectSpecific(Vec), } impl fmt::Display for ColumnOption { @@ -199,11 +290,12 @@ impl fmt::Display for ColumnOption { Ok(()) } Check(expr) => write!(f, "CHECK ({})", expr), + DialectSpecific(val) => write!(f, "{}", display_separated(val, " ")), } } } -fn display_constraint_name<'a>(name: &'a Option) -> impl fmt::Display + 'a { +fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { struct ConstraintName<'a>(&'a Option); impl<'a> fmt::Display for ConstraintName<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -220,7 +312,8 @@ fn display_constraint_name<'a>(name: &'a Option) -> impl fmt::Display + ' /// { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }` /// /// Used in foreign key constraints in `ON UPDATE` and `ON DELETE` options. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ReferentialAction { Restrict, Cascade, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index aa531d5f83..e9afae5746 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -18,6 +18,7 @@ mod operator; mod query; mod value; +#[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::fmt; @@ -28,8 +29,9 @@ pub use self::ddl::{ }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - Cte, Fetch, Join, JoinConstraint, JoinOperator, Offset, OffsetRows, OrderByExpr, Query, Select, - SelectItem, SetExpr, SetOperator, TableAlias, TableFactor, TableWithJoins, Top, Values, + Cte, Fetch, Join, JoinConstraint, JoinOperator, LateralView, Offset, OffsetRows, OrderByExpr, + Query, Select, SelectItem, SetExpr, SetOperator, TableAlias, TableFactor, TableWithJoins, Top, + Values, With, }; pub use self::value::{DateTimeField, Value}; @@ -71,7 +73,8 @@ where } /// An identifier, decomposed into its value or character data and the quote style. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Ident { /// The value of the identifier without quotes. pub value: String, @@ -127,7 +130,8 @@ impl fmt::Display for Ident { } /// A name of a table, view, custom type, etc., possibly multi-part, i.e. db.schema.obj -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ObjectName(pub Vec); impl fmt::Display for ObjectName { @@ -141,7 +145,8 @@ impl fmt::Display for ObjectName { /// The parser does not distinguish between expressions of different types /// (e.g. boolean vs string), so the caller must handle expressions of /// inappropriate type, like `WHERE 1` or `SELECT 1=1`, as necessary. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Expr { /// Identifier e.g. table name or column name Identifier(Ident), @@ -187,16 +192,26 @@ pub enum Expr { right: Box, }, /// Unary operation e.g. `NOT foo` - UnaryOp { op: UnaryOperator, expr: Box }, + UnaryOp { + op: UnaryOperator, + expr: Box, + }, /// CAST an expression to a different data type e.g. `CAST(foo AS VARCHAR(123))` Cast { expr: Box, data_type: DataType, }, + /// EXTRACT(DateTimeField FROM ) Extract { field: DateTimeField, expr: Box, }, + /// SUBSTRING( [FROM ] [FOR ]) + Substring { + expr: Box, + substring_from: Option>, + substring_for: Option>, + }, /// `expr COLLATE collation` Collate { expr: Box, @@ -206,6 +221,17 @@ pub enum Expr { Nested(Box), /// A literal value, such as string, number, date or NULL Value(Value), + /// A constant of form ` 'value'`. + /// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`), + /// as well as constants of other types (a non-standard PostgreSQL extension). + TypedString { + data_type: DataType, + value: String, + }, + MapAccess { + column: Box, + key: String, + }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), /// `CASE [] [ELSE ] END` @@ -228,6 +254,7 @@ impl fmt::Display for Expr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Expr::Identifier(s) => write!(f, "{}", s), + Expr::MapAccess { column, key } => write!(f, "{}[\"{}\"]", column, key), Expr::Wildcard => f.write_str("*"), Expr::QualifiedWildcard(q) => write!(f, "{}.*", display_separated(q, ".")), Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")), @@ -269,19 +296,29 @@ impl fmt::Display for Expr { high ), Expr::BinaryOp { left, op, right } => write!(f, "{} {} {}", left, op, right), - Expr::UnaryOp { op, expr } => write!(f, "{} {}", op, expr), + Expr::UnaryOp { op, expr } => { + if op == &UnaryOperator::PGPostfixFactorial { + write!(f, "{}{}", expr, op) + } else { + write!(f, "{} {}", op, expr) + } + } Expr::Cast { expr, data_type } => write!(f, "CAST({} AS {})", expr, data_type), Expr::Extract { field, expr } => write!(f, "EXTRACT({} FROM {})", field, expr), Expr::Collate { expr, collation } => write!(f, "{} COLLATE {}", expr, collation), Expr::Nested(ast) => write!(f, "({})", ast), Expr::Value(v) => write!(f, "{}", v), + Expr::TypedString { data_type, value } => { + write!(f, "{}", data_type)?; + write!(f, " '{}'", &value::escape_single_quote_string(value)) + } Expr::Function(fun) => write!(f, "{}", fun), Expr::Case { operand, when_clauses, else_result, } => { - f.write_str("CASE")?; + write!(f, "CASE")?; if let Some(operand) = operand { write!(f, " {}", operand)?; } @@ -290,11 +327,26 @@ impl fmt::Display for Expr { if let Some(else_result) = else_result { write!(f, " ELSE {}", else_result)?; } - f.write_str(" END") + write!(f, " END") } Expr::Exists(s) => write!(f, "EXISTS ({})", s), Expr::Subquery(s) => write!(f, "({})", s), Expr::ListAgg(listagg) => write!(f, "{}", listagg), + Expr::Substring { + expr, + substring_from, + substring_for, + } => { + write!(f, "SUBSTRING({}", expr)?; + if let Some(from_part) = substring_from { + write!(f, " FROM {}", from_part)?; + } + if let Some(from_part) = substring_for { + write!(f, " FOR {}", from_part)?; + } + + write!(f, ")") + } } } } @@ -304,7 +356,8 @@ impl fmt::Display for Expr { /// Note: we only recognize a complete single expression as ``, /// not `< 0` nor `1, 2, 3` as allowed in a `` per /// -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct WhenClause { pub condition: Expr, pub result: Expr, @@ -317,7 +370,8 @@ impl fmt::Display for WhenClause { } /// A window specification (i.e. `OVER (PARTITION BY .. ORDER BY .. etc.)`) -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct WindowSpec { pub partition_by: Vec, pub order_by: Vec, @@ -362,7 +416,8 @@ impl fmt::Display for WindowSpec { /// /// Note: The parser does not validate the specified bounds; the caller should /// reject invalid bounds like `ROWS UNBOUNDED FOLLOWING` before execution. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct WindowFrame { pub units: WindowFrameUnits, pub start_bound: WindowFrameBound, @@ -373,7 +428,8 @@ pub struct WindowFrame { // TBD: EXCLUDE } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum WindowFrameUnits { Rows, Range, @@ -390,24 +446,9 @@ impl fmt::Display for WindowFrameUnits { } } -impl FromStr for WindowFrameUnits { - type Err = ParserError; - - fn from_str(s: &str) -> Result { - match s { - "ROWS" => Ok(WindowFrameUnits::Rows), - "RANGE" => Ok(WindowFrameUnits::Range), - "GROUPS" => Ok(WindowFrameUnits::Groups), - _ => Err(ParserError::ParserError(format!( - "Expected ROWS, RANGE, or GROUPS, found: {}", - s - ))), - } - } -} - /// Specifies [WindowFrame]'s `start_bound` and `end_bound` -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum WindowFrameBound { /// `CURRENT ROW` CurrentRow, @@ -429,20 +470,78 @@ impl fmt::Display for WindowFrameBound { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum AddDropSync { + ADD, + DROP, + SYNC, +} + +impl fmt::Display for AddDropSync { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AddDropSync::SYNC => f.write_str("SYNC PARTITIONS"), + AddDropSync::DROP => f.write_str("DROP PARTITIONS"), + AddDropSync::ADD => f.write_str("ADD PARTITIONS"), + } + } +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Statement { + /// Analyze (Hive) + Analyze { + table_name: ObjectName, + partitions: Option>, + for_columns: bool, + columns: Vec, + cache_metadata: bool, + noscan: bool, + compute_statistics: bool, + }, + /// Truncate (Hive) + Truncate { + table_name: ObjectName, + partitions: Option>, + }, + /// Msck (Hive) + Msck { + table_name: ObjectName, + repair: bool, + partition_action: Option, + }, /// SELECT Query(Box), /// INSERT Insert { + /// Only for Sqlite + or: Option, /// TABLE table_name: ObjectName, /// COLUMNS columns: Vec, + /// Overwrite (Hive) + overwrite: bool, /// A SQL query that specifies what to insert source: Box, + /// partitioned insert (Hive) + partitioned: Option>, + /// Columns defined after PARTITION + after_columns: Vec, + /// whether the insert has the table keyword (Hive) + table: bool, + }, + // TODO: Support ROW FORMAT + Directory { + overwrite: bool, + local: bool, + path: String, + file_format: Option, + source: Box, }, Copy { /// TABLE @@ -470,32 +569,48 @@ pub enum Statement { }, /// CREATE VIEW CreateView { + or_replace: bool, + materialized: bool, /// View name name: ObjectName, columns: Vec, query: Box, - materialized: bool, with_options: Vec, }, /// CREATE TABLE CreateTable { + or_replace: bool, + temporary: bool, + external: bool, + if_not_exists: bool, /// Table name name: ObjectName, /// Optional schema columns: Vec, constraints: Vec, + hive_distribution: HiveDistributionStyle, + hive_formats: Option, + table_properties: Vec, with_options: Vec, - if_not_exists: bool, - external: bool, file_format: Option, location: Option, + query: Option>, + without_rowid: bool, + like: Option, + }, + /// SQLite's `CREATE VIRTUAL TABLE .. USING ()` + CreateVirtualTable { + name: ObjectName, + if_not_exists: bool, + module_name: Ident, + module_args: Vec, }, /// CREATE INDEX CreateIndex { /// index name name: ObjectName, table_name: ObjectName, - columns: Vec, + columns: Vec, unique: bool, if_not_exists: bool, }, @@ -516,6 +631,9 @@ pub enum Statement { /// Whether `CASCADE` was specified. This will be `false` when /// `RESTRICT` or no drop behavior at all was specified. cascade: bool, + /// Hive allows you specify whether the table's stored data will be + /// deleted along with the dropped table + purge: bool, }, /// SET /// @@ -524,13 +642,14 @@ pub enum Statement { /// supported yet. SetVariable { local: bool, + hivevar: bool, variable: Ident, - value: SetVariableValue, + value: Vec, }, /// SHOW /// /// Note: this is a PostgreSQL-specific statement. - ShowVariable { variable: Ident }, + ShowVariable { variable: Vec }, /// SHOW COLUMNS /// /// Note: this is a MySQL-specific statement. @@ -549,7 +668,47 @@ pub enum Statement { /// `ROLLBACK [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ]` Rollback { chain: bool }, /// CREATE SCHEMA - CreateSchema { schema_name: ObjectName }, + CreateSchema { + schema_name: ObjectName, + if_not_exists: bool, + }, + /// CREATE DATABASE + CreateDatabase { + db_name: ObjectName, + if_not_exists: bool, + location: Option, + managed_location: Option, + }, + /// `ASSERT [AS ]` + Assert { + condition: Expr, + message: Option, + }, + /// `DEALLOCATE [ PREPARE ] { name | ALL }` + /// + /// Note: this is a PostgreSQL-specific statement. + Deallocate { name: Ident, prepare: bool }, + /// `EXECUTE name [ ( parameter [, ...] ) ]` + /// + /// Note: this is a PostgreSQL-specific statement. + Execute { name: Ident, parameters: Vec }, + /// `PREPARE name [ ( data_type [, ...] ) ] AS statement` + /// + /// Note: this is a PostgreSQL-specific statement. + Prepare { + name: Ident, + data_types: Vec, + statement: Box, + }, + /// EXPLAIN + Explain { + /// Carry out the command and show actual run times and other statistics. + analyze: bool, + // Display additional information regarding the plan. + verbose: bool, + /// A SQL query that specifies what to explain + statement: Box, + }, } impl fmt::Display for Statement { @@ -558,18 +717,139 @@ impl fmt::Display for Statement { #[allow(clippy::cognitive_complexity)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + Statement::Explain { + verbose, + analyze, + statement, + } => { + write!(f, "EXPLAIN ")?; + + if *analyze { + write!(f, "ANALYZE ")?; + } + + if *verbose { + write!(f, "VERBOSE ")?; + } + + write!(f, "{}", statement) + } Statement::Query(s) => write!(f, "{}", s), + Statement::Directory { + overwrite, + local, + path, + file_format, + source, + } => { + write!( + f, + "INSERT{overwrite}{local} DIRECTORY '{path}'", + overwrite = if *overwrite { " OVERWRITE" } else { "" }, + local = if *local { " LOCAL" } else { "" }, + path = path + )?; + if let Some(ref ff) = file_format { + write!(f, " STORED AS {}", ff)? + } + write!(f, " {}", source) + } + Statement::Msck { + table_name, + repair, + partition_action, + } => { + write!( + f, + "MSCK {repair}TABLE {table}", + repair = if *repair { "REPAIR " } else { "" }, + table = table_name + )?; + if let Some(pa) = partition_action { + write!(f, " {}", pa)?; + } + Ok(()) + } + Statement::Truncate { + table_name, + partitions, + } => { + write!(f, "TRUNCATE TABLE {}", table_name)?; + if let Some(ref parts) = partitions { + if !parts.is_empty() { + write!(f, " PARTITION ({})", display_comma_separated(parts))?; + } + } + Ok(()) + } + Statement::Analyze { + table_name, + partitions, + for_columns, + columns, + cache_metadata, + noscan, + compute_statistics, + } => { + write!(f, "ANALYZE TABLE {}", table_name)?; + if let Some(ref parts) = partitions { + if !parts.is_empty() { + write!(f, " PARTITION ({})", display_comma_separated(parts))?; + } + } + + if *compute_statistics { + write!(f, " COMPUTE STATISTICS")?; + } + if *noscan { + write!(f, " NOSCAN")?; + } + if *cache_metadata { + write!(f, " CACHE METADATA")?; + } + if *for_columns { + write!(f, " FOR COLUMNS")?; + if !columns.is_empty() { + write!(f, " {}", display_comma_separated(columns))?; + } + } + Ok(()) + } Statement::Insert { + or, table_name, + overwrite, + partitioned, columns, + after_columns, source, + table, } => { - write!(f, "INSERT INTO {} ", table_name)?; + if let Some(action) = or { + write!(f, "INSERT OR {} INTO {} ", action, table_name)?; + } else { + write!( + f, + "INSERT {act}{tbl} {table_name} ", + table_name = table_name, + act = if *overwrite { "OVERWRITE" } else { "INTO" }, + tbl = if *table { " TABLE" } else { "" } + )?; + } if !columns.is_empty() { write!(f, "({}) ", display_comma_separated(columns))?; } + if let Some(ref parts) = partitioned { + if !parts.is_empty() { + write!(f, "PARTITION ({}) ", display_comma_separated(parts))?; + } + } + if !after_columns.is_empty() { + write!(f, "({}) ", display_comma_separated(after_columns))?; + } write!(f, "{}", source) } + Statement::Copy { table_name, columns, @@ -602,8 +882,7 @@ impl fmt::Display for Statement { } => { write!(f, "UPDATE {}", table_name)?; if !assignments.is_empty() { - write!(f, " SET ")?; - write!(f, "{}", display_comma_separated(assignments))?; + write!(f, " SET {}", display_comma_separated(assignments))?; } if let Some(selection) = selection { write!(f, " WHERE {}", selection)?; @@ -620,53 +899,169 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateDatabase { + db_name, + if_not_exists, + location, + managed_location, + } => { + write!(f, "CREATE")?; + if *if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " {}", db_name)?; + if let Some(l) = location { + write!(f, " LOCATION '{}'", l)?; + } + if let Some(ml) = managed_location { + write!(f, " MANAGEDLOCATION '{}'", ml)?; + } + Ok(()) + } Statement::CreateView { name, + or_replace, columns, query, materialized, with_options, } => { - write!(f, "CREATE")?; - if *materialized { - write!(f, " MATERIALIZED")?; - } - - write!(f, " VIEW {}", name)?; - + write!( + f, + "CREATE {or_replace}{materialized}VIEW {name}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + materialized = if *materialized { "MATERIALIZED " } else { "" }, + name = name + )?; if !with_options.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_options))?; } - if !columns.is_empty() { write!(f, " ({})", display_comma_separated(columns))?; } - write!(f, " AS {}", query) } Statement::CreateTable { name, columns, constraints, + table_properties, with_options, + or_replace, if_not_exists, + hive_distribution, + hive_formats, external, + temporary, file_format, location, + query, + without_rowid, + like, } => { + // We want to allow the following options + // Empty column list, allowed by PostgreSQL: + // `CREATE TABLE t ()` + // No columns provided for CREATE TABLE AS: + // `CREATE TABLE t AS SELECT a from t2` + // Columns provided for CREATE TABLE AS: + // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {}TABLE {}{} ({}", - if *external { "EXTERNAL " } else { "" }, - if *if_not_exists { "IF NOT EXISTS " } else { "" }, - name, - display_comma_separated(columns) + "CREATE {or_replace}{external}{temporary}TABLE {if_not_exists}{name}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + external = if *external { "EXTERNAL " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + temporary = if *temporary { "TEMPORARY " } else { "" }, + name = name, )?; - if !constraints.is_empty() { - write!(f, ", {}", display_comma_separated(constraints))?; + if !columns.is_empty() || !constraints.is_empty() { + write!(f, " ({}", display_comma_separated(columns))?; + if !columns.is_empty() && !constraints.is_empty() { + write!(f, ", ")?; + } + write!(f, "{})", display_comma_separated(constraints))?; + } else if query.is_none() && like.is_none() { + // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens + write!(f, " ()")?; + } + // Only for SQLite + if *without_rowid { + write!(f, " WITHOUT ROWID")?; } - write!(f, ")")?; + // Only for Hive + if let Some(l) = like { + write!(f, " LIKE {}", l)?; + } + match hive_distribution { + HiveDistributionStyle::PARTITIONED { columns } => { + write!(f, " PARTITIONED BY ({})", display_comma_separated(&columns))?; + } + HiveDistributionStyle::CLUSTERED { + columns, + sorted_by, + num_buckets, + } => { + write!(f, " CLUSTERED BY ({})", display_comma_separated(&columns))?; + if !sorted_by.is_empty() { + write!(f, " SORTED BY ({})", display_comma_separated(&sorted_by))?; + } + if *num_buckets > 0 { + write!(f, " INTO {} BUCKETS", num_buckets)?; + } + } + HiveDistributionStyle::SKEWED { + columns, + on, + stored_as_directories, + } => { + write!( + f, + " SKEWED BY ({})) ON ({})", + display_comma_separated(&columns), + display_comma_separated(&on) + )?; + if *stored_as_directories { + write!(f, " STORED AS DIRECTORIES")?; + } + } + _ => (), + } + + if let Some(HiveFormat { + row_format, + storage, + location, + }) = hive_formats + { + match row_format { + Some(HiveRowFormat::SERDE { class }) => { + write!(f, " ROW FORMAT SERDE '{}'", class)? + } + Some(HiveRowFormat::DELIMITED) => write!(f, " ROW FORMAT DELIMITED")?, + None => (), + } + match storage { + Some(HiveIOFormat::IOF { + input_format, + output_format, + }) => write!( + f, + " STORED AS INPUTFORMAT {} OUTPUTFORMAT {}", + input_format, output_format + )?, + Some(HiveIOFormat::FileFormat { format }) if !*external => { + write!(f, " STORED AS {}", format)? + } + _ => (), + } + if !*external { + if let Some(loc) = location { + write!(f, " LOCATION '{}'", loc)?; + } + } + } if *external { write!( f, @@ -675,33 +1070,54 @@ impl fmt::Display for Statement { location.as_ref().unwrap() )?; } + if !table_properties.is_empty() { + write!( + f, + " TBLPROPERTIES ({})", + display_comma_separated(table_properties) + )?; + } if !with_options.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_options))?; } + if let Some(query) = query { + write!(f, " AS {}", query)?; + } Ok(()) } - Statement::CreateIndex { + Statement::CreateVirtualTable { name, - table_name, - columns, - unique, if_not_exists, + module_name, + module_args, } => { write!( f, - "CREATE{}INDEX{}{} ON {}({}", - if *unique { " UNIQUE " } else { " " }, - if *if_not_exists { - " IF NOT EXISTS " - } else { - " " - }, - name, - table_name, - display_separated(columns, ",") + "CREATE VIRTUAL TABLE {if_not_exists}{name} USING {module_name}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + name = name, + module_name = module_name )?; - write!(f, ");") + if !module_args.is_empty() { + write!(f, " ({})", display_comma_separated(module_args))?; + } + Ok(()) } + Statement::CreateIndex { + name, + table_name, + columns, + unique, + if_not_exists, + } => write!( + f, + "CREATE {unique}INDEX {if_not_exists}{name} ON {table_name}({columns})", + unique = if *unique { "UNIQUE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + name = name, + table_name = table_name, + columns = display_separated(columns, ",") + ), Statement::AlterTable { name, operation } => { write!(f, "ALTER TABLE {} {}", name, operation) } @@ -710,40 +1126,54 @@ impl fmt::Display for Statement { if_exists, names, cascade, + purge, } => write!( f, - "DROP {}{} {}{}", + "DROP {}{} {}{}{}", object_type, if *if_exists { " IF EXISTS" } else { "" }, display_comma_separated(names), if *cascade { " CASCADE" } else { "" }, + if *purge { " PURGE" } else { "" } ), Statement::SetVariable { local, variable, + hivevar, value, } => { f.write_str("SET ")?; if *local { f.write_str("LOCAL ")?; } - write!(f, "{} = {}", variable, value) + write!( + f, + "{hivevar}{name} = {value}", + hivevar = if *hivevar { "HIVEVAR:" } else { "" }, + name = variable, + value = display_comma_separated(value) + ) + } + Statement::ShowVariable { variable } => { + write!(f, "SHOW")?; + if !variable.is_empty() { + write!(f, " {}", display_separated(variable, " "))?; + } + Ok(()) } - Statement::ShowVariable { variable } => write!(f, "SHOW {}", variable), Statement::ShowColumns { extended, full, table_name, filter, } => { - f.write_str("SHOW ")?; - if *extended { - f.write_str("EXTENDED ")?; - } - if *full { - f.write_str("FULL ")?; - } - write!(f, "COLUMNS FROM {}", table_name)?; + write!( + f, + "SHOW {extended}{full}COLUMNS FROM {table_name}", + extended = if *extended { "EXTENDED " } else { "" }, + full = if *full { "FULL " } else { "" }, + table_name = table_name, + )?; if let Some(filter) = filter { write!(f, " {}", filter)?; } @@ -769,13 +1199,53 @@ impl fmt::Display for Statement { Statement::Rollback { chain } => { write!(f, "ROLLBACK{}", if *chain { " AND CHAIN" } else { "" },) } - Statement::CreateSchema { schema_name } => write!(f, "CREATE SCHEMA {}", schema_name), + Statement::CreateSchema { + schema_name, + if_not_exists, + } => write!( + f, + "CREATE SCHEMA {if_not_exists}{name}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + name = schema_name + ), + Statement::Assert { condition, message } => { + write!(f, "ASSERT {}", condition)?; + if let Some(m) = message { + write!(f, " AS {}", m)?; + } + Ok(()) + } + Statement::Deallocate { name, prepare } => write!( + f, + "DEALLOCATE {prepare}{name}", + prepare = if *prepare { "PREPARE " } else { "" }, + name = name, + ), + Statement::Execute { name, parameters } => { + write!(f, "EXECUTE {}", name)?; + if !parameters.is_empty() { + write!(f, "({})", display_comma_separated(parameters))?; + } + Ok(()) + } + Statement::Prepare { + name, + data_types, + statement, + } => { + write!(f, "PREPARE {} ", name)?; + if !data_types.is_empty() { + write!(f, "({}) ", display_comma_separated(data_types))?; + } + write!(f, "AS {}", statement) + } } } } /// SQL assignment `foo = expr` as used in SQLUpdate -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Assignment { pub id: Ident, pub value: Expr, @@ -787,11 +1257,28 @@ impl fmt::Display for Assignment { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum FunctionArg { + Named { name: Ident, arg: Expr }, + Unnamed(Expr), +} + +impl fmt::Display for FunctionArg { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FunctionArg::Named { name, arg } => write!(f, "{} => {}", name, arg), + FunctionArg::Unnamed(unnamed_arg) => write!(f, "{}", unnamed_arg), + } + } +} + /// A function call -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Function { pub name: ObjectName, - pub args: Vec, + pub args: Vec, pub over: Option, // aggregate functions may specify eg `COUNT(DISTINCT x)` pub distinct: bool, @@ -814,7 +1301,8 @@ impl fmt::Display for Function { } /// External table's available file format -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum FileFormat { TEXTFILE, SEQUENCEFILE, @@ -835,37 +1323,15 @@ impl fmt::Display for FileFormat { PARQUET => "PARQUET", AVRO => "AVRO", RCFILE => "RCFILE", - JSONFILE => "TEXTFILE", + JSONFILE => "JSONFILE", }) } } -use crate::parser::ParserError; -use std::str::FromStr; -impl FromStr for FileFormat { - type Err = ParserError; - - fn from_str(s: &str) -> Result { - use self::FileFormat::*; - match s { - "TEXTFILE" => Ok(TEXTFILE), - "SEQUENCEFILE" => Ok(SEQUENCEFILE), - "ORC" => Ok(ORC), - "PARQUET" => Ok(PARQUET), - "AVRO" => Ok(AVRO), - "RCFILE" => Ok(RCFILE), - "JSONFILE" => Ok(JSONFILE), - _ => Err(ParserError::ParserError(format!( - "Unexpected file format: {}", - s - ))), - } - } -} - /// A `LISTAGG` invocation `LISTAGG( [ DISTINCT ] [, ] [ON OVERFLOW ] ) ) /// [ WITHIN GROUP (ORDER BY [, ...] ) ]` -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ListAgg { pub distinct: bool, pub expr: Box, @@ -901,7 +1367,8 @@ impl fmt::Display for ListAgg { } /// The `ON OVERFLOW` clause of a LISTAGG invocation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ListAggOnOverflow { /// `ON OVERFLOW ERROR` Error, @@ -934,7 +1401,8 @@ impl fmt::Display for ListAggOnOverflow { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ObjectType { Table, View, @@ -953,7 +1421,64 @@ impl fmt::Display for ObjectType { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum HiveDistributionStyle { + PARTITIONED { + columns: Vec, + }, + CLUSTERED { + columns: Vec, + sorted_by: Vec, + num_buckets: i32, + }, + SKEWED { + columns: Vec, + on: Vec, + stored_as_directories: bool, + }, + NONE, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum HiveRowFormat { + SERDE { class: String }, + DELIMITED, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum HiveIOFormat { + IOF { + input_format: Expr, + output_format: Expr, + }, + FileFormat { + format: FileFormat, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct HiveFormat { + pub row_format: Option, + pub storage: Option, + pub location: Option, +} + +impl Default for HiveFormat { + fn default() -> Self { + HiveFormat { + row_format: None, + location: None, + storage: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SqlOption { pub name: Ident, pub value: Value, @@ -965,7 +1490,8 @@ impl fmt::Display for SqlOption { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TransactionMode { AccessMode(TransactionAccessMode), IsolationLevel(TransactionIsolationLevel), @@ -981,7 +1507,8 @@ impl fmt::Display for TransactionMode { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TransactionAccessMode { ReadOnly, ReadWrite, @@ -997,7 +1524,8 @@ impl fmt::Display for TransactionAccessMode { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TransactionIsolationLevel { ReadUncommitted, ReadCommitted, @@ -1017,7 +1545,8 @@ impl fmt::Display for TransactionIsolationLevel { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ShowStatementFilter { Like(String), Where(Expr), @@ -1033,7 +1562,8 @@ impl fmt::Display for ShowStatementFilter { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SetVariableValue { Ident(Ident), Literal(Value), @@ -1048,3 +1578,29 @@ impl fmt::Display for SetVariableValue { } } } + +/// Sqlite specific syntax +/// +/// https://sqlite.org/lang_conflict.html +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum SqliteOnConflict { + Rollback, + Abort, + Fail, + Ignore, + Replace, +} + +impl fmt::Display for SqliteOnConflict { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use SqliteOnConflict::*; + match self { + Rollback => write!(f, "ROLLBACK"), + Abort => write!(f, "ABORT"), + Fail => write!(f, "FAIL"), + Ignore => write!(f, "IGNORE"), + Replace => write!(f, "REPLACE"), + } + } +} diff --git a/src/ast/operator.rs b/src/ast/operator.rs index b112b080c8..732c812322 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -10,15 +10,29 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::fmt; /// Unary operators -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum UnaryOperator { Plus, Minus, Not, + /// Bitwise Not, e.g. `~9` (PostgreSQL-specific) + PGBitwiseNot, + /// Square root, e.g. `|/9` (PostgreSQL-specific) + PGSquareRoot, + /// Cube root, e.g. `||/27` (PostgreSQL-specific) + PGCubeRoot, + /// Factorial, e.g. `9!` (PostgreSQL-specific) + PGPostfixFactorial, + /// Factorial, e.g. `!!9` (PostgreSQL-specific) + PGPrefixFactorial, + /// Absolute value, e.g. `@ -9` (PostgreSQL-specific) + PGAbs, } impl fmt::Display for UnaryOperator { @@ -27,12 +41,19 @@ impl fmt::Display for UnaryOperator { UnaryOperator::Plus => "+", UnaryOperator::Minus => "-", UnaryOperator::Not => "NOT", + UnaryOperator::PGBitwiseNot => "~", + UnaryOperator::PGSquareRoot => "|/", + UnaryOperator::PGCubeRoot => "||/", + UnaryOperator::PGPostfixFactorial => "!", + UnaryOperator::PGPrefixFactorial => "!!", + UnaryOperator::PGAbs => "@", }) } } /// Binary operators -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum BinaryOperator { Plus, Minus, @@ -44,6 +65,7 @@ pub enum BinaryOperator { Lt, GtEq, LtEq, + Spaceship, Eq, NotEq, And, @@ -53,6 +75,9 @@ pub enum BinaryOperator { BitwiseOr, BitwiseAnd, BitwiseXor, + PGBitwiseXor, + PGBitwiseShiftLeft, + PGBitwiseShiftRight, } impl fmt::Display for BinaryOperator { @@ -68,6 +93,7 @@ impl fmt::Display for BinaryOperator { BinaryOperator::Lt => "<", BinaryOperator::GtEq => ">=", BinaryOperator::LtEq => "<=", + BinaryOperator::Spaceship => "<=>", BinaryOperator::Eq => "=", BinaryOperator::NotEq => "<>", BinaryOperator::And => "AND", @@ -77,6 +103,9 @@ impl fmt::Display for BinaryOperator { BinaryOperator::BitwiseOr => "|", BinaryOperator::BitwiseAnd => "&", BinaryOperator::BitwiseXor => "^", + BinaryOperator::PGBitwiseXor => "#", + BinaryOperator::PGBitwiseShiftLeft => "<<", + BinaryOperator::PGBitwiseShiftRight => ">>", }) } } diff --git a/src/ast/query.rs b/src/ast/query.rs index 239ace7de0..8f9ab499de 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -11,14 +11,17 @@ // limitations under the License. use super::*; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; /// The most complete variant of a `SELECT` query expression, optionally /// including `WITH`, `UNION` / other set operations, and `ORDER BY`. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Query { /// WITH (common table expressions, or CTEs) - pub ctes: Vec, - /// SELECT or UNION / EXCEPT / INTECEPT + pub with: Option, + /// SELECT or UNION / EXCEPT / INTERSECT pub body: SetExpr, /// ORDER BY pub order_by: Vec, @@ -32,8 +35,8 @@ pub struct Query { impl fmt::Display for Query { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if !self.ctes.is_empty() { - write!(f, "WITH {} ", display_comma_separated(&self.ctes))?; + if let Some(ref with) = self.with { + write!(f, "{} ", with)?; } write!(f, "{}", self.body)?; if !self.order_by.is_empty() { @@ -54,7 +57,9 @@ impl fmt::Display for Query { /// A node in a tree, representing a "query body" expression, roughly: /// `SELECT ... [ {UNION|EXCEPT|INTERSECT} SELECT ...]` -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SetExpr { /// Restricted SELECT .. FROM .. HAVING (no ORDER BY or set operations) Select(Box