diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml new file mode 100644 index 000000000..c851bff31 --- /dev/null +++ b/.github/workflows/license.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +name: license + +# trigger for all PRs and changes to main +on: + push: + branches: + - main + pull_request: + +jobs: + + rat: + name: Release Audit Tool (RAT) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.8 + - name: Audit licenses + run: ./dev/release/run-rat.sh . diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b5744e863..3abf9d387 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,6 +19,9 @@ name: Rust on: [push, pull_request] +permissions: + contents: read + jobs: codestyle: @@ -85,11 +88,8 @@ jobs: uses: ./.github/actions/setup-builder with: rust-version: ${{ matrix.rust }} + - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Install Tarpaulin - uses: actions-rs/install@v0.1 - with: - crate: cargo-tarpaulin - version: 0.14.2 - use-tool-cache: true + run: cargo install cargo-tarpaulin - name: Test run: cargo test --all-features diff --git a/Cargo.toml b/Cargo.toml index d746775e4..2e48f9d0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.56.0" +version = "0.58.0" authors = ["Apache DataFusion "] homepage = "https://github.com/apache/datafusion-sqlparser-rs" documentation = "https://docs.rs/sqlparser/" diff --git a/README.md b/README.md index 6acfbcef7..9dfe50810 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ [![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://github.com/sqlparser-rs/sqlparser-rs/workflows/Rust/badge.svg?branch=main)](https://github.com/sqlparser-rs/sqlparser-rs/actions?query=workflow%3ARust+branch%3Amain) +[![Build Status](https://github.com/apache/datafusion-sqlparser-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/sqlparser-rs/sqlparser-rs/actions?query=workflow%3ARust+branch%3Amain) [![Coverage Status](https://coveralls.io/repos/github/sqlparser-rs/sqlparser-rs/badge.svg?branch=main)](https://coveralls.io/github/sqlparser-rs/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) @@ -89,10 +89,14 @@ keywords, the following should hold true for all SQL: ```rust // Parse SQL +let sql = "SELECT 'hello'"; let ast = Parser::parse_sql(&GenericDialect, sql).unwrap(); // The original SQL text can be generated from the AST assert_eq!(ast[0].to_string(), sql); + +// The SQL can also be pretty-printed with newlines and indentation +assert_eq!(format!("{:#}", ast[0]), "SELECT\n 'hello'"); ``` There are still some cases in this crate where different SQL with seemingly diff --git a/changelog/0.57.0.md b/changelog/0.57.0.md new file mode 100644 index 000000000..200bb73af --- /dev/null +++ b/changelog/0.57.0.md @@ -0,0 +1,95 @@ + + +# sqlparser-rs 0.57.0 Changelog + +This release consists of 39 commits from 19 contributors. See credits at the end of this changelog for more information. + +**Implemented enhancements:** + +- feat: Hive: support `SORT BY` direction [#1873](https://github.com/apache/datafusion-sqlparser-rs/pull/1873) (chenkovsky) + +**Other:** + +- Support some of pipe operators [#1759](https://github.com/apache/datafusion-sqlparser-rs/pull/1759) (simonvandel) +- Added support for `DROP DOMAIN` [#1828](https://github.com/apache/datafusion-sqlparser-rs/pull/1828) (LucaCappelletti94) +- Improve support for cursors for SQL Server [#1831](https://github.com/apache/datafusion-sqlparser-rs/pull/1831) (aharpervc) +- Add all missing table options to be handled in any order [#1747](https://github.com/apache/datafusion-sqlparser-rs/pull/1747) (benrsatori) +- Add `CREATE TRIGGER` support for SQL Server [#1810](https://github.com/apache/datafusion-sqlparser-rs/pull/1810) (aharpervc) +- Added support for `CREATE DOMAIN` [#1830](https://github.com/apache/datafusion-sqlparser-rs/pull/1830) (LucaCappelletti94) +- Allow stored procedures to be defined without `BEGIN`/`END` [#1834](https://github.com/apache/datafusion-sqlparser-rs/pull/1834) (aharpervc) +- Add support for the MATCH and REGEXP binary operators [#1840](https://github.com/apache/datafusion-sqlparser-rs/pull/1840) (lovasoa) +- Fix: parsing ident starting with underscore in certain dialects [#1835](https://github.com/apache/datafusion-sqlparser-rs/pull/1835) (MohamedAbdeen21) +- implement pretty-printing with `{:#}` [#1847](https://github.com/apache/datafusion-sqlparser-rs/pull/1847) (lovasoa) +- Fix big performance issue in string serialization [#1848](https://github.com/apache/datafusion-sqlparser-rs/pull/1848) (lovasoa) +- Add support for `DENY` statements [#1836](https://github.com/apache/datafusion-sqlparser-rs/pull/1836) (aharpervc) +- Postgresql: Add `REPLICA IDENTITY` operation for `ALTER TABLE` [#1844](https://github.com/apache/datafusion-sqlparser-rs/pull/1844) (MohamedAbdeen21) +- Add support for INCLUDE/EXCLUDE NULLS for UNPIVOT [#1849](https://github.com/apache/datafusion-sqlparser-rs/pull/1849) (Vedin) +- pretty print improvements [#1851](https://github.com/apache/datafusion-sqlparser-rs/pull/1851) (lovasoa) +- fix new rust 1.87 cargo clippy warnings [#1856](https://github.com/apache/datafusion-sqlparser-rs/pull/1856) (lovasoa) +- Update criterion requirement from 0.5 to 0.6 in /sqlparser_bench [#1857](https://github.com/apache/datafusion-sqlparser-rs/pull/1857) (dependabot[bot]) +- pretty-print CREATE TABLE statements [#1854](https://github.com/apache/datafusion-sqlparser-rs/pull/1854) (lovasoa) +- pretty-print CREATE VIEW statements [#1855](https://github.com/apache/datafusion-sqlparser-rs/pull/1855) (lovasoa) +- Handle optional datatypes properly in `CREATE FUNCTION` statements [#1826](https://github.com/apache/datafusion-sqlparser-rs/pull/1826) (LucaCappelletti94) +- Mysql: Add `SRID` column option [#1852](https://github.com/apache/datafusion-sqlparser-rs/pull/1852) (MohamedAbdeen21) +- Add support for table valued functions for SQL Server [#1839](https://github.com/apache/datafusion-sqlparser-rs/pull/1839) (aharpervc) +- Keep the COLUMN keyword only if it exists when dropping the column [#1862](https://github.com/apache/datafusion-sqlparser-rs/pull/1862) (git-hulk) +- Add support for parameter default values in SQL Server [#1866](https://github.com/apache/datafusion-sqlparser-rs/pull/1866) (aharpervc) +- Add support for `TABLESAMPLE` pipe operator [#1860](https://github.com/apache/datafusion-sqlparser-rs/pull/1860) (hendrikmakait) +- Adds support for mysql's drop index [#1864](https://github.com/apache/datafusion-sqlparser-rs/pull/1864) (dmzmk) +- Fix: GROUPING SETS accept values without parenthesis [#1867](https://github.com/apache/datafusion-sqlparser-rs/pull/1867) (Vedin) +- Add ICEBERG keyword support to ALTER TABLE statement [#1869](https://github.com/apache/datafusion-sqlparser-rs/pull/1869) (osipovartem) +- MySQL: Support `index_name` in FK constraints [#1871](https://github.com/apache/datafusion-sqlparser-rs/pull/1871) (MohamedAbdeen21) +- Postgres: Apply `ONLY` keyword per table in TRUNCATE stmt [#1872](https://github.com/apache/datafusion-sqlparser-rs/pull/1872) (MohamedAbdeen21) +- Fix `CASE` expression spans [#1874](https://github.com/apache/datafusion-sqlparser-rs/pull/1874) (eliaperantoni) +- MySQL: `[[NOT] ENFORCED]` in CHECK constraint [#1870](https://github.com/apache/datafusion-sqlparser-rs/pull/1870) (MohamedAbdeen21) +- Add support for `CREATE SCHEMA WITH ( )` [#1877](https://github.com/apache/datafusion-sqlparser-rs/pull/1877) (utay) +- Add support for `ALTER TABLE DROP INDEX` [#1865](https://github.com/apache/datafusion-sqlparser-rs/pull/1865) (vimko) +- chore: Replace archived actions-rs/install action [#1876](https://github.com/apache/datafusion-sqlparser-rs/pull/1876) (assignUser) +- Allow `IF NOT EXISTS` after table name for Snowflake [#1881](https://github.com/apache/datafusion-sqlparser-rs/pull/1881) (bombsimon) +- Support `DISTINCT AS { STRUCT | VALUE }` for BigQuery [#1880](https://github.com/apache/datafusion-sqlparser-rs/pull/1880) (bombsimon) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 7 Ophir LOJKINE + 6 Andrew Harper + 6 Mohamed Abdeen + 3 Luca Cappelletti + 2 Denys Tsomenko + 2 Simon Sawert + 1 Andrew Lamb + 1 Artem Osipov + 1 Chen Chongchen + 1 Dmitriy Mazurin + 1 Elia Perantoni + 1 Hendrik Makait + 1 Jacob Wujciak-Jens + 1 Simon Vandel Sillesen + 1 Yannick Utard + 1 benrsatori + 1 dependabot[bot] + 1 hulk + 1 vimko +``` + +Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. + diff --git a/changelog/0.58.0.md b/changelog/0.58.0.md new file mode 100644 index 000000000..27a985d7c --- /dev/null +++ b/changelog/0.58.0.md @@ -0,0 +1,106 @@ + + +# sqlparser-rs 0.58.0 Changelog + +This release consists of 47 commits from 18 contributors. See credits at the end of this changelog for more information. + +**Fixed bugs:** + +- fix: parse snowflake fetch clause [#1894](https://github.com/apache/datafusion-sqlparser-rs/pull/1894) (Vedin) + +**Documentation updates:** + +- docs: Update rust badge [#1943](https://github.com/apache/datafusion-sqlparser-rs/pull/1943) (Olexandr88) + +**Other:** + +- Add license header check to CI [#1888](https://github.com/apache/datafusion-sqlparser-rs/pull/1888) (alamb) +- Add support of parsing struct field's options in BigQuery [#1890](https://github.com/apache/datafusion-sqlparser-rs/pull/1890) (git-hulk) +- Fix parsing error when having fields after nested struct in BigQuery [#1897](https://github.com/apache/datafusion-sqlparser-rs/pull/1897) (git-hulk) +- Extend exception handling [#1884](https://github.com/apache/datafusion-sqlparser-rs/pull/1884) (bombsimon) +- Postgres: Add support for text search types [#1889](https://github.com/apache/datafusion-sqlparser-rs/pull/1889) (MohamedAbdeen21) +- Fix `limit` in subqueries [#1899](https://github.com/apache/datafusion-sqlparser-rs/pull/1899) (Dimchikkk) +- Use `IndexColumn` in all index definitions [#1900](https://github.com/apache/datafusion-sqlparser-rs/pull/1900) (mvzink) +- Support procedure argmode [#1901](https://github.com/apache/datafusion-sqlparser-rs/pull/1901) (ZacJW) +- Fix `impl Ord for Ident` [#1893](https://github.com/apache/datafusion-sqlparser-rs/pull/1893) (eliaperantoni) +- Snowflake: support multiple column options in `CREATE VIEW` [#1891](https://github.com/apache/datafusion-sqlparser-rs/pull/1891) (eliaperantoni) +- Add support for `LANGUAGE` clause in `CREATE PROCEDURE` [#1903](https://github.com/apache/datafusion-sqlparser-rs/pull/1903) (ZacJW) +- Fix clippy lints on 1.88.0 [#1910](https://github.com/apache/datafusion-sqlparser-rs/pull/1910) (iffyio) +- Snowflake: Add support for future grants [#1906](https://github.com/apache/datafusion-sqlparser-rs/pull/1906) (yoavcloud) +- Support for Map values in ClickHouse settings [#1896](https://github.com/apache/datafusion-sqlparser-rs/pull/1896) (solontsev) +- Fix join precedence for non-snowflake queries [#1905](https://github.com/apache/datafusion-sqlparser-rs/pull/1905) (Dimchikkk) +- Support remaining pipe operators [#1879](https://github.com/apache/datafusion-sqlparser-rs/pull/1879) (simonvandel) +- Make `GenericDialect` support from-first syntax [#1911](https://github.com/apache/datafusion-sqlparser-rs/pull/1911) (simonvandel) +- Redshift utf8 idents [#1915](https://github.com/apache/datafusion-sqlparser-rs/pull/1915) (yoavcloud) +- DuckDB: Add support for multiple `TRIM` arguments [#1916](https://github.com/apache/datafusion-sqlparser-rs/pull/1916) (ryanschneider) +- Redshift alter column type no set [#1912](https://github.com/apache/datafusion-sqlparser-rs/pull/1912) (yoavcloud) +- Postgres: support `ADD CONSTRAINT NOT VALID` and `VALIDATE CONSTRAINT` [#1908](https://github.com/apache/datafusion-sqlparser-rs/pull/1908) (achristmascarl) +- Add support for MySQL MEMBER OF [#1917](https://github.com/apache/datafusion-sqlparser-rs/pull/1917) (yoavcloud) +- Add span for `Expr::TypedString` [#1919](https://github.com/apache/datafusion-sqlparser-rs/pull/1919) (feral-dot-io) +- Support for Postgres `CREATE SERVER` [#1914](https://github.com/apache/datafusion-sqlparser-rs/pull/1914) (solontsev) +- Change tag and policy names to `ObjectName` [#1892](https://github.com/apache/datafusion-sqlparser-rs/pull/1892) (eliaperantoni) +- Add support for NULL escape char in pattern match searches [#1913](https://github.com/apache/datafusion-sqlparser-rs/pull/1913) (yoavcloud) +- Add support for dropping multiple columns in Snowflake [#1918](https://github.com/apache/datafusion-sqlparser-rs/pull/1918) (yoavcloud) +- Align Snowflake dialect to new test of reserved keywords [#1924](https://github.com/apache/datafusion-sqlparser-rs/pull/1924) (yoavcloud) +- Make `GenericDialect` support trailing commas in projections [#1921](https://github.com/apache/datafusion-sqlparser-rs/pull/1921) (simonvandel) +- Add support for several Snowflake grant statements [#1922](https://github.com/apache/datafusion-sqlparser-rs/pull/1922) (yoavcloud) +- Clickhouse: support empty parenthesized options [#1925](https://github.com/apache/datafusion-sqlparser-rs/pull/1925) (solontsev) +- Add Snowflake `COPY/REVOKE CURRENT GRANTS` option [#1926](https://github.com/apache/datafusion-sqlparser-rs/pull/1926) (yoavcloud) +- Add support for Snowflake identifier function [#1929](https://github.com/apache/datafusion-sqlparser-rs/pull/1929) (yoavcloud) +- Add support for granting privileges to procedures and functions in Snowflake [#1930](https://github.com/apache/datafusion-sqlparser-rs/pull/1930) (yoavcloud) +- Add support for `+` char in Snowflake stage names [#1935](https://github.com/apache/datafusion-sqlparser-rs/pull/1935) (yoavcloud) +- Snowflake Reserved SQL Keywords as Implicit Table Alias [#1934](https://github.com/apache/datafusion-sqlparser-rs/pull/1934) (yoavcloud) +- Add support for Redshift `SELECT * EXCLUDE` [#1936](https://github.com/apache/datafusion-sqlparser-rs/pull/1936) (yoavcloud) +- Support optional semicolon between statements [#1937](https://github.com/apache/datafusion-sqlparser-rs/pull/1937) (yoavcloud) +- Snowflake: support trailing options in `CREATE TABLE` [#1931](https://github.com/apache/datafusion-sqlparser-rs/pull/1931) (yoavcloud) +- MSSQL: Add support for EXEC output and default keywords [#1940](https://github.com/apache/datafusion-sqlparser-rs/pull/1940) (yoavcloud) +- Add identifier unicode support in Mysql, Postgres and Redshift [#1933](https://github.com/apache/datafusion-sqlparser-rs/pull/1933) (etgarperets) +- Add identifier start unicode support for Postegres, MySql and Redshift [#1944](https://github.com/apache/datafusion-sqlparser-rs/pull/1944) (etgarperets) +- Fix for Postgres regex and like binary operators [#1928](https://github.com/apache/datafusion-sqlparser-rs/pull/1928) (solontsev) +- Snowflake: Improve accuracy of lookahead in implicit LIMIT alias [#1941](https://github.com/apache/datafusion-sqlparser-rs/pull/1941) (yoavcloud) +- Add support for `DROP USER` statement [#1951](https://github.com/apache/datafusion-sqlparser-rs/pull/1951) (yoavcloud) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 19 Yoav Cohen + 4 Sergey Olontsev + 3 Elia Perantoni + 3 Simon Vandel Sillesen + 2 Dima + 2 ZacJW + 2 etgarperets + 2 hulk + 1 Andrew Lamb + 1 Denys Tsomenko + 1 Ifeanyi Ubah + 1 Michael Victor Zink + 1 Mohamed Abdeen + 1 Olexandr88 + 1 Ryan Schneider + 1 Simon Sawert + 1 carl + 1 feral-dot-io +``` + +Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. + diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index 562eec2f1..280b1bce6 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -1,7 +1,8 @@ -# Files to exclude from the Apache Rat (license) check -.gitignore .tool-versions +target/* +**.gitignore +rat.txt dev/release/rat_exclude_files.txt -fuzz/.gitignore sqlparser_bench/img/flamegraph.svg - +**Cargo.lock +filtered_rat.txt diff --git a/examples/cli.rs b/examples/cli.rs index 0252fca74..08a40a6dd 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -63,7 +63,7 @@ $ cargo run --example cli - [--dialectname] }; let contents = if filename == "-" { - println!("Parsing from stdin using {:?}", dialect); + println!("Parsing from stdin using {dialect:?}"); let mut buf = Vec::new(); stdin() .read_to_end(&mut buf) diff --git a/sqlparser_bench/Cargo.toml b/sqlparser_bench/Cargo.toml index 2c1f0ae4d..4fb9af16e 100644 --- a/sqlparser_bench/Cargo.toml +++ b/sqlparser_bench/Cargo.toml @@ -26,7 +26,7 @@ edition = "2018" sqlparser = { path = "../" } [dev-dependencies] -criterion = "0.5" +criterion = "0.7" [[bench]] name = "sqlparser_bench" diff --git a/sqlparser_bench/benches/sqlparser_bench.rs b/sqlparser_bench/benches/sqlparser_bench.rs index a7768cbc9..6132ee432 100644 --- a/sqlparser_bench/benches/sqlparser_bench.rs +++ b/sqlparser_bench/benches/sqlparser_bench.rs @@ -45,30 +45,29 @@ fn basic_queries(c: &mut Criterion) { let large_statement = { let expressions = (0..1000) - .map(|n| format!("FN_{}(COL_{})", n, n)) + .map(|n| format!("FN_{n}(COL_{n})")) .collect::>() .join(", "); let tables = (0..1000) - .map(|n| format!("TABLE_{}", n)) + .map(|n| format!("TABLE_{n}")) .collect::>() .join(" JOIN "); let where_condition = (0..1000) - .map(|n| format!("COL_{} = {}", n, n)) + .map(|n| format!("COL_{n} = {n}")) .collect::>() .join(" OR "); let order_condition = (0..1000) - .map(|n| format!("COL_{} DESC", n)) + .map(|n| format!("COL_{n} DESC")) .collect::>() .join(", "); format!( - "SELECT {} FROM {} WHERE {} ORDER BY {}", - expressions, tables, where_condition, order_condition + "SELECT {expressions} FROM {tables} WHERE {where_condition} ORDER BY {order_condition}" ) }; group.bench_function("parse_large_statement", |b| { - b.iter(|| Parser::parse_sql(&dialect, criterion::black_box(large_statement.as_str()))); + b.iter(|| Parser::parse_sql(&dialect, std::hint::black_box(large_statement.as_str()))); }); let large_statement = Parser::parse_sql(&dialect, large_statement.as_str()) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 52919de8a..dde9e0b7e 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -48,7 +48,17 @@ pub enum DataType { /// Table type in [PostgreSQL], e.g. CREATE FUNCTION RETURNS TABLE(...). /// /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html - Table(Vec), + /// [MsSQL]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#c-create-a-multi-statement-table-valued-function + Table(Option>), + /// Table type with a name, e.g. CREATE FUNCTION RETURNS @result TABLE(...). + /// + /// [MsSQl]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#table + NamedTable { + /// Table name. + name: ObjectName, + /// Table columns. + columns: Vec, + }, /// Fixed-length character type, e.g. CHARACTER(10). Character(Option), /// Fixed-length char type, e.g. CHAR(10). @@ -336,7 +346,16 @@ pub enum DataType { /// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type TimestampNtz, /// Interval type. - Interval, + Interval { + /// [PostgreSQL] fields specification like `INTERVAL YEAR TO MONTH`. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-datetime.html + fields: Option, + /// [PostgreSQL] subsecond precision like `INTERVAL HOUR TO SECOND(3)` + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-datetime.html + precision: Option, + }, /// JSON type. JSON, /// Binary JSON type. @@ -436,6 +455,14 @@ pub enum DataType { /// /// [PostgreSQL]: https://www.postgresql.org/docs/9.5/functions-geometry.html GeometricType(GeometricTypeKind), + /// PostgreSQL text search vectors, see [PostgreSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-textsearch.html + TsVector, + /// PostgreSQL text search query, see [PostgreSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-textsearch.html + TsQuery, } impl fmt::Display for DataType { @@ -617,7 +644,16 @@ impl fmt::Display for DataType { timezone, ) } - DataType::Interval => write!(f, "INTERVAL"), + DataType::Interval { fields, precision } => { + write!(f, "INTERVAL")?; + if let Some(fields) = fields { + write!(f, " {fields}")?; + } + if let Some(precision) = precision { + write!(f, "({precision})")?; + } + Ok(()) + } DataType::JSON => write!(f, "JSON"), DataType::JSONB => write!(f, "JSONB"), DataType::Regclass => write!(f, "REGCLASS"), @@ -648,7 +684,7 @@ impl fmt::Display for DataType { } DataType::Enum(vals, bits) => { match bits { - Some(bits) => write!(f, "ENUM{}", bits), + Some(bits) => write!(f, "ENUM{bits}"), None => write!(f, "ENUM"), }?; write!(f, "(")?; @@ -696,16 +732,16 @@ impl fmt::Display for DataType { } // ClickHouse DataType::Nullable(data_type) => { - write!(f, "Nullable({})", data_type) + write!(f, "Nullable({data_type})") } DataType::FixedString(character_length) => { - write!(f, "FixedString({})", character_length) + write!(f, "FixedString({character_length})") } DataType::LowCardinality(data_type) => { - write!(f, "LowCardinality({})", data_type) + write!(f, "LowCardinality({data_type})") } DataType::Map(key_data_type, value_data_type) => { - write!(f, "Map({}, {})", key_data_type, value_data_type) + write!(f, "Map({key_data_type}, {value_data_type})") } DataType::Tuple(fields) => { write!(f, "Tuple({})", display_comma_separated(fields)) @@ -716,8 +752,20 @@ impl fmt::Display for DataType { DataType::Unspecified => Ok(()), DataType::Trigger => write!(f, "TRIGGER"), DataType::AnyType => write!(f, "ANY TYPE"), - DataType::Table(fields) => write!(f, "TABLE({})", display_comma_separated(fields)), - DataType::GeometricType(kind) => write!(f, "{}", kind), + DataType::Table(fields) => match fields { + Some(fields) => { + write!(f, "TABLE({})", display_comma_separated(fields)) + } + None => { + write!(f, "TABLE") + } + }, + DataType::NamedTable { name, columns } => { + write!(f, "{} TABLE ({})", name, display_comma_separated(columns)) + } + DataType::GeometricType(kind) => write!(f, "{kind}"), + DataType::TsVector => write!(f, "TSVECTOR"), + DataType::TsQuery => write!(f, "TSQUERY"), } } } @@ -859,6 +907,48 @@ impl fmt::Display for TimezoneInfo { } } +/// Fields for [Postgres] `INTERVAL` type. +/// +/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum IntervalFields { + Year, + Month, + Day, + Hour, + Minute, + Second, + YearToMonth, + DayToHour, + DayToMinute, + DayToSecond, + HourToMinute, + HourToSecond, + MinuteToSecond, +} + +impl fmt::Display for IntervalFields { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IntervalFields::Year => write!(f, "YEAR"), + IntervalFields::Month => write!(f, "MONTH"), + IntervalFields::Day => write!(f, "DAY"), + IntervalFields::Hour => write!(f, "HOUR"), + IntervalFields::Minute => write!(f, "MINUTE"), + IntervalFields::Second => write!(f, "SECOND"), + IntervalFields::YearToMonth => write!(f, "YEAR TO MONTH"), + IntervalFields::DayToHour => write!(f, "DAY TO HOUR"), + IntervalFields::DayToMinute => write!(f, "DAY TO MINUTE"), + IntervalFields::DayToSecond => write!(f, "DAY TO SECOND"), + IntervalFields::HourToMinute => write!(f, "HOUR TO MINUTE"), + IntervalFields::HourToSecond => write!(f, "HOUR TO SECOND"), + IntervalFields::MinuteToSecond => write!(f, "MINUTE TO SECOND"), + } + } +} + /// Additional information for `NUMERIC`, `DECIMAL`, and `DEC` data types /// following the 2016 [SQL Standard]. /// @@ -872,7 +962,7 @@ pub enum ExactNumberInfo { /// Only precision information, e.g. `DECIMAL(10)` Precision(u64), /// Precision and scale information, e.g. `DECIMAL(10,2)` - PrecisionAndScale(u64, u64), + PrecisionAndScale(u64, i64), } impl fmt::Display for ExactNumberInfo { @@ -912,7 +1002,7 @@ impl fmt::Display for CharacterLength { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CharacterLength::IntegerLength { length, unit } => { - write!(f, "{}", length)?; + write!(f, "{length}")?; if let Some(unit) = unit { write!(f, " {unit}")?; } @@ -967,7 +1057,7 @@ impl fmt::Display for BinaryLength { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { BinaryLength::IntegerLength { length } => { - write!(f, "{}", length)?; + write!(f, "{length}")?; } BinaryLength::Max => { write!(f, "MAX")?; diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 735ab0cce..079894075 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -173,7 +173,7 @@ impl fmt::Display for AlterRoleOperation { in_database, } => { if let Some(database_name) = in_database { - write!(f, "IN DATABASE {} ", database_name)?; + write!(f, "IN DATABASE {database_name} ")?; } match config_value { @@ -187,7 +187,7 @@ impl fmt::Display for AlterRoleOperation { in_database, } => { if let Some(database_name) = in_database { - write!(f, "IN DATABASE {} ", database_name)?; + write!(f, "IN DATABASE {database_name} ")?; } match config_name { @@ -218,15 +218,15 @@ impl fmt::Display for Use { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("USE ")?; match self { - Use::Catalog(name) => write!(f, "CATALOG {}", name), - Use::Schema(name) => write!(f, "SCHEMA {}", name), - Use::Database(name) => write!(f, "DATABASE {}", name), - Use::Warehouse(name) => write!(f, "WAREHOUSE {}", name), - Use::Role(name) => write!(f, "ROLE {}", name), + Use::Catalog(name) => write!(f, "CATALOG {name}"), + Use::Schema(name) => write!(f, "SCHEMA {name}"), + Use::Database(name) => write!(f, "DATABASE {name}"), + Use::Warehouse(name) => write!(f, "WAREHOUSE {name}"), + Use::Role(name) => write!(f, "ROLE {name}"), Use::SecondaryRoles(secondary_roles) => { - write!(f, "SECONDARY ROLES {}", secondary_roles) + write!(f, "SECONDARY ROLES {secondary_roles}") } - Use::Object(name) => write!(f, "{}", name), + Use::Object(name) => write!(f, "{name}"), Use::Default => write!(f, "DEFAULT"), } } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c1c113b32..db1cb61f6 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,22 +30,71 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, CommentDef, CreateFunctionBody, - CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, - FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, - OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value, - ValueWithSpan, + display_comma_separated, display_separated, ArgMode, CommentDef, CreateFunctionBody, + CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, DataType, Expr, FileFormat, + FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, + HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InitializeKind, + MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg, + OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, SequenceOptions, + Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, Value, ValueWithSpan, + WrappedCollection, }; +use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; use crate::keywords::Keyword; -use crate::tokenizer::Token; +use crate::tokenizer::{Span, Token}; + +/// Index column type. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IndexColumn { + pub column: OrderByExpr, + pub operator_class: Option, +} + +impl fmt::Display for IndexColumn { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.column)?; + if let Some(operator_class) = &self.operator_class { + write!(f, " {operator_class}")?; + } + Ok(()) + } +} + +/// ALTER TABLE operation REPLICA IDENTITY values +/// See [Postgres ALTER TABLE docs](https://www.postgresql.org/docs/current/sql-altertable.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ReplicaIdentity { + None, + Full, + Default, + Index(Ident), +} + +impl fmt::Display for ReplicaIdentity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ReplicaIdentity::None => f.write_str("NONE"), + ReplicaIdentity::Full => f.write_str("FULL"), + ReplicaIdentity::Default => f.write_str("DEFAULT"), + ReplicaIdentity::Index(idx) => write!(f, "USING INDEX {idx}"), + } + } +} /// An `ALTER TABLE` (`Statement::AlterTable`) operation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum AlterTableOperation { - /// `ADD ` - AddConstraint(TableConstraint), + /// `ADD [NOT VALID]` + AddConstraint { + constraint: TableConstraint, + not_valid: bool, + }, /// `ADD [COLUMN] [IF NOT EXISTS] ` AddColumn { /// `[COLUMN]`. @@ -114,15 +163,16 @@ pub enum AlterTableOperation { name: Ident, drop_behavior: Option, }, - /// `DROP [ COLUMN ] [ IF EXISTS ] [ CASCADE ]` + /// `DROP [ COLUMN ] [ IF EXISTS ] [ , , ... ] [ CASCADE ]` DropColumn { - column_name: Ident, + has_column_keyword: bool, + column_names: Vec, if_exists: bool, drop_behavior: Option, }, /// `ATTACH PART|PARTITION ` /// Note: this is a ClickHouse-specific operation, please refer to - /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/pakrtition#attach-partitionpart) + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/partition#attach-partitionpart) AttachPartition { // PART is not a short form of PARTITION, it's a separate keyword // which represents a physical file on disk and partition is a logical entity. @@ -151,16 +201,23 @@ pub enum AlterTableOperation { }, /// `DROP PRIMARY KEY` /// - /// Note: this is a [MySQL]-specific operation. - /// - /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html - DropPrimaryKey, + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/alter-table.html) + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/constraints-drop) + DropPrimaryKey { + drop_behavior: Option, + }, /// `DROP FOREIGN KEY ` /// - /// Note: this is a [MySQL]-specific operation. + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/alter-table.html) + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/constraints-drop) + DropForeignKey { + name: Ident, + drop_behavior: Option, + }, + /// `DROP INDEX ` /// /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html - DropForeignKey { + DropIndex { name: Ident, }, /// `ENABLE ALWAYS RULE rewrite_rule_name` @@ -208,6 +265,13 @@ pub enum AlterTableOperation { old_partitions: Vec, new_partitions: Vec, }, + /// REPLICA IDENTITY { DEFAULT | USING INDEX index_name | FULL | NOTHING } + /// + /// Note: this is a PostgreSQL-specific operation. + /// Please refer to [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-altertable.html) + ReplicaIdentity { + identity: ReplicaIdentity, + }, /// Add Partitions AddPartitions { if_not_exists: bool, @@ -224,7 +288,7 @@ pub enum AlterTableOperation { }, /// `RENAME TO ` RenameTable { - table_name: ObjectName, + table_name: RenameTableNameKind, }, // CHANGE [ COLUMN ] [ ] ChangeColumn { @@ -307,6 +371,20 @@ pub enum AlterTableOperation { equals: bool, value: ValueWithSpan, }, + /// `VALIDATE CONSTRAINT ` + ValidateConstraint { + name: Ident, + }, + /// Arbitrary parenthesized `SET` options. + /// + /// Example: + /// ```sql + /// SET (scale_factor = 0.01, threshold = 500)` + /// ``` + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertable.html) + SetOptionsParens { + options: Vec, + }, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -413,7 +491,7 @@ pub enum Owner { impl fmt::Display for Owner { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Owner::Ident(ident) => write!(f, "{}", ident), + Owner::Ident(ident) => write!(f, "{ident}"), Owner::CurrentRole => write!(f, "CURRENT_ROLE"), Owner::CurrentUser => write!(f, "CURRENT_USER"), Owner::SessionUser => write!(f, "SESSION_USER"), @@ -457,7 +535,16 @@ impl fmt::Display for AlterTableOperation { display_separated(new_partitions, " "), ine = if *if_not_exists { " IF NOT EXISTS" } else { "" } ), - AlterTableOperation::AddConstraint(c) => write!(f, "ADD {c}"), + AlterTableOperation::AddConstraint { + not_valid, + constraint, + } => { + write!(f, "ADD {constraint}")?; + if *not_valid { + write!(f, " NOT VALID")?; + } + Ok(()) + } AlterTableOperation::AddColumn { column_keyword, if_not_exists, @@ -488,7 +575,7 @@ impl fmt::Display for AlterTableOperation { if *if_not_exists { write!(f, " IF NOT EXISTS")?; } - write!(f, " {} ({})", name, query) + write!(f, " {name} ({query})") } AlterTableOperation::Algorithm { equals, algorithm } => { write!( @@ -503,7 +590,7 @@ impl fmt::Display for AlterTableOperation { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name) + write!(f, " {name}") } AlterTableOperation::MaterializeProjection { if_exists, @@ -514,9 +601,9 @@ impl fmt::Display for AlterTableOperation { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name)?; + write!(f, " {name}")?; if let Some(partition) = partition { - write!(f, " IN PARTITION {}", partition)?; + write!(f, " IN PARTITION {partition}")?; } Ok(()) } @@ -529,9 +616,9 @@ impl fmt::Display for AlterTableOperation { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name)?; + write!(f, " {name}")?; if let Some(partition) = partition { - write!(f, " IN PARTITION {}", partition)?; + write!(f, " IN PARTITION {partition}")?; } Ok(()) } @@ -563,33 +650,51 @@ impl fmt::Display for AlterTableOperation { } => { write!( f, - "DROP CONSTRAINT {}{}{}", + "DROP CONSTRAINT {}{}", if *if_exists { "IF EXISTS " } else { "" }, - name, - match drop_behavior { - None => "", - Some(DropBehavior::Restrict) => " RESTRICT", - Some(DropBehavior::Cascade) => " CASCADE", - } - ) + name + )?; + if let Some(drop_behavior) = drop_behavior { + write!(f, " {drop_behavior}")?; + } + Ok(()) } - AlterTableOperation::DropPrimaryKey => write!(f, "DROP PRIMARY KEY"), - AlterTableOperation::DropForeignKey { name } => write!(f, "DROP FOREIGN KEY {name}"), + AlterTableOperation::DropPrimaryKey { drop_behavior } => { + write!(f, "DROP PRIMARY KEY")?; + if let Some(drop_behavior) = drop_behavior { + write!(f, " {drop_behavior}")?; + } + Ok(()) + } + AlterTableOperation::DropForeignKey { + name, + drop_behavior, + } => { + write!(f, "DROP FOREIGN KEY {name}")?; + if let Some(drop_behavior) = drop_behavior { + write!(f, " {drop_behavior}")?; + } + Ok(()) + } + AlterTableOperation::DropIndex { name } => write!(f, "DROP INDEX {name}"), AlterTableOperation::DropColumn { - column_name, + has_column_keyword, + column_names: column_name, if_exists, drop_behavior, - } => write!( - f, - "DROP COLUMN {}{}{}", - if *if_exists { "IF EXISTS " } else { "" }, - column_name, - match drop_behavior { - None => "", - Some(DropBehavior::Restrict) => " RESTRICT", - Some(DropBehavior::Cascade) => " CASCADE", + } => { + write!( + f, + "DROP {}{}{}", + if *has_column_keyword { "COLUMN " } else { "" }, + if *if_exists { "IF EXISTS " } else { "" }, + display_comma_separated(column_name), + )?; + if let Some(drop_behavior) = drop_behavior { + write!(f, " {drop_behavior}")?; } - ), + Ok(()) + } AlterTableOperation::AttachPartition { partition } => { write!(f, "ATTACH {partition}") } @@ -631,7 +736,7 @@ impl fmt::Display for AlterTableOperation { new_column_name, } => write!(f, "RENAME COLUMN {old_column_name} TO {new_column_name}"), AlterTableOperation::RenameTable { table_name } => { - write!(f, "RENAME TO {table_name}") + write!(f, "RENAME {table_name}") } AlterTableOperation::ChangeColumn { old_name, @@ -729,6 +834,15 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::Lock { equals, lock } => { write!(f, "LOCK {}{}", if *equals { "= " } else { "" }, lock) } + AlterTableOperation::ReplicaIdentity { identity } => { + write!(f, "REPLICA IDENTITY {identity}") + } + AlterTableOperation::ValidateConstraint { name } => { + write!(f, "VALIDATE CONSTRAINT {name}") + } + AlterTableOperation::SetOptionsParens { options } => { + write!(f, "SET ({})", display_comma_separated(options)) + } } } } @@ -850,7 +964,10 @@ pub enum AlterColumnOperation { data_type: DataType, /// PostgreSQL specific using: Option, + /// Set to true if the statement includes the `SET DATA TYPE` keywords + had_set: bool, }, + /// `ADD GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) ]` /// /// Note: this is a PostgreSQL-specific operation. @@ -871,12 +988,19 @@ impl fmt::Display for AlterColumnOperation { AlterColumnOperation::DropDefault => { write!(f, "DROP DEFAULT") } - AlterColumnOperation::SetDataType { data_type, using } => { + AlterColumnOperation::SetDataType { + data_type, + using, + had_set, + } => { + if *had_set { + write!(f, "SET DATA ")?; + } + write!(f, "TYPE {data_type}")?; if let Some(expr) = using { - write!(f, "SET DATA TYPE {data_type} USING {expr}") - } else { - write!(f, "SET DATA TYPE {data_type}") + write!(f, " USING {expr}")?; } + Ok(()) } AlterColumnOperation::AddGenerated { generated_as, @@ -936,7 +1060,7 @@ pub enum TableConstraint { /// [1]: IndexType index_type: Option, /// Identifiers of the columns that are unique. - columns: Vec, + columns: Vec, index_options: Vec, characteristics: Option, /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` @@ -972,7 +1096,7 @@ pub enum TableConstraint { /// [1]: IndexType index_type: Option, /// Identifiers of the columns that form the primary key. - columns: Vec, + columns: Vec, index_options: Vec, characteristics: Option, }, @@ -983,6 +1107,9 @@ pub enum TableConstraint { /// }`). ForeignKey { name: Option, + /// MySQL-specific field + /// + index_name: Option, columns: Vec, foreign_table: ObjectName, referred_columns: Vec, @@ -990,10 +1117,13 @@ pub enum TableConstraint { on_update: Option, characteristics: Option, }, - /// `[ CONSTRAINT ] CHECK ()` + /// `[ CONSTRAINT ] CHECK () [[NOT] ENFORCED]` Check { name: Option, expr: Box, + /// MySQL-specific syntax + /// + enforced: Option, }, /// MySQLs [index definition][1] for index creation. Not present on ANSI so, for now, the usage /// is restricted to MySQL, as no other dialects that support this syntax were found. @@ -1011,7 +1141,9 @@ pub enum TableConstraint { /// [1]: IndexType index_type: Option, /// Referred column identifier list. - columns: Vec, + columns: Vec, + /// Optional index options such as `USING`; see [`IndexOption`]. + index_options: Vec, }, /// MySQLs [fulltext][1] definition. Since the [`SPATIAL`][2] definition is exactly the same, /// and MySQL displays both the same way, it is part of this definition as well. @@ -1034,7 +1166,7 @@ pub enum TableConstraint { /// Optional index name. opt_index_name: Option, /// Referred column identifier list. - columns: Vec, + columns: Vec, }, } @@ -1093,6 +1225,7 @@ impl fmt::Display for TableConstraint { } TableConstraint::ForeignKey { name, + index_name, columns, foreign_table, referred_columns, @@ -1102,8 +1235,9 @@ impl fmt::Display for TableConstraint { } => { write!( f, - "{}FOREIGN KEY ({}) REFERENCES {}", + "{}FOREIGN KEY{} ({}) REFERENCES {}", display_constraint_name(name), + display_option_spaced(index_name), display_comma_separated(columns), foreign_table, )?; @@ -1117,18 +1251,28 @@ impl fmt::Display for TableConstraint { write!(f, " ON UPDATE {action}")?; } if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + write!(f, " {characteristics}")?; } Ok(()) } - TableConstraint::Check { name, expr } => { - write!(f, "{}CHECK ({})", display_constraint_name(name), expr) + TableConstraint::Check { + name, + expr, + enforced, + } => { + write!(f, "{}CHECK ({})", display_constraint_name(name), expr)?; + if let Some(b) = enforced { + write!(f, " {}", if *b { "ENFORCED" } else { "NOT ENFORCED" }) + } else { + Ok(()) + } } TableConstraint::Index { display_as_key, name, index_type, columns, + index_options, } => { write!(f, "{}", if *display_as_key { "KEY" } else { "INDEX" })?; if let Some(name) = name { @@ -1138,7 +1282,9 @@ impl fmt::Display for TableConstraint { write!(f, " USING {index_type}")?; } write!(f, " ({})", display_comma_separated(columns))?; - + if !index_options.is_empty() { + write!(f, " {}", display_comma_separated(index_options))?; + } Ok(()) } Self::FulltextOrSpatial { @@ -1248,22 +1394,25 @@ impl fmt::Display for IndexType { Self::SPGiST => write!(f, "SPGIST"), Self::BRIN => write!(f, "BRIN"), Self::Bloom => write!(f, "BLOOM"), - Self::Custom(name) => write!(f, "{}", name), + Self::Custom(name) => write!(f, "{name}"), } } } -/// MySQLs index option. -/// -/// This structure used here [`MySQL` CREATE TABLE][1], [`MySQL` CREATE INDEX][2]. +/// MySQL index option, used in [`CREATE TABLE`], [`CREATE INDEX`], and [`ALTER TABLE`]. /// -/// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html -/// [2]: https://dev.mysql.com/doc/refman/8.3/en/create-index.html +/// [`CREATE TABLE`]: https://dev.mysql.com/doc/refman/8.4/en/create-table.html +/// [`CREATE INDEX`]: https://dev.mysql.com/doc/refman/8.4/en/create-index.html +/// [`ALTER TABLE`]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum IndexOption { + /// `USING { BTREE | HASH }`: Index type to use for the index. + /// + /// Note that we permissively parse non-MySQL index types, like `GIN`. Using(IndexType), + /// `COMMENT 'string'`: Specifies a comment for the index. Comment(String), } @@ -1307,11 +1456,16 @@ impl fmt::Display for NullsDistinctOption { pub struct ProcedureParam { pub name: Ident, pub data_type: DataType, + pub mode: Option, } impl fmt::Display for ProcedureParam { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} {}", self.name, self.data_type) + if let Some(mode) = &self.mode { + write!(f, "{mode} {} {}", self.name, self.data_type) + } else { + write!(f, "{} {}", self.name, self.data_type) + } } } @@ -1361,17 +1515,41 @@ impl fmt::Display for ColumnDef { pub struct ViewColumnDef { pub name: Ident, pub data_type: Option, - pub options: Option>, + pub options: Option, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ColumnOptions { + CommaSeparated(Vec), + SpaceSeparated(Vec), +} + +impl ColumnOptions { + pub fn as_slice(&self) -> &[ColumnOption] { + match self { + ColumnOptions::CommaSeparated(options) => options.as_slice(), + ColumnOptions::SpaceSeparated(options) => options.as_slice(), + } + } } impl fmt::Display for ViewColumnDef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; if let Some(data_type) = self.data_type.as_ref() { - write!(f, " {}", data_type)?; + write!(f, " {data_type}")?; } if let Some(options) = self.options.as_ref() { - write!(f, " {}", display_comma_separated(options.as_slice()))?; + match options { + ColumnOptions::CommaSeparated(column_options) => { + write!(f, " {}", display_comma_separated(column_options.as_slice()))?; + } + ColumnOptions::SpaceSeparated(column_options) => { + write!(f, " {}", display_separated(column_options.as_slice(), " "))? + } + } } Ok(()) } @@ -1591,7 +1769,7 @@ pub struct ColumnPolicyProperty { /// ``` /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table pub with: bool, - pub policy_name: Ident, + pub policy_name: ObjectName, pub using_columns: Option>, } @@ -1725,6 +1903,13 @@ pub enum ColumnOption { /// ``` /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table Tags(TagsColumnOption), + /// MySQL specific: Spatial reference identifier + /// Syntax: + /// ```sql + /// CREATE TABLE geom (g GEOMETRY NOT NULL SRID 4326); + /// ``` + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/creating-spatial-indexes.html + Srid(Box), } impl fmt::Display for ColumnOption { @@ -1749,7 +1934,7 @@ impl fmt::Display for ColumnOption { } => { write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" })?; if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + write!(f, " {characteristics}")?; } Ok(()) } @@ -1771,7 +1956,7 @@ impl fmt::Display for ColumnOption { write!(f, " ON UPDATE {action}")?; } if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + write!(f, " {characteristics}")?; } Ok(()) } @@ -1831,7 +2016,7 @@ impl fmt::Display for ColumnOption { write!(f, "{parameters}") } OnConflict(keyword) => { - write!(f, "ON CONFLICT {:?}", keyword)?; + write!(f, "ON CONFLICT {keyword:?}")?; Ok(()) } Policy(parameters) => { @@ -1840,6 +2025,9 @@ impl fmt::Display for ColumnOption { Tags(tags) => { write!(f, "{tags}") } + Srid(srid) => { + write!(f, "SRID {srid}") + } } } } @@ -2153,6 +2341,550 @@ impl fmt::Display for ClusteredBy { } } +/// CREATE INDEX statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateIndex { + /// index name + pub name: Option, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + pub using: Option, + pub columns: Vec, + pub unique: bool, + pub concurrently: bool, + pub if_not_exists: bool, + pub include: Vec, + pub nulls_distinct: Option, + /// WITH clause: + pub with: Vec, + pub predicate: Option, + pub index_options: Vec, + /// [MySQL] allows a subset of options normally used for `ALTER TABLE`: + /// + /// - `ALGORITHM` + /// - `LOCK` + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/create-index.html + pub alter_options: Vec, +} + +impl fmt::Display for CreateIndex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE {unique}INDEX {concurrently}{if_not_exists}", + unique = if self.unique { "UNIQUE " } else { "" }, + concurrently = if self.concurrently { + "CONCURRENTLY " + } else { + "" + }, + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + )?; + if let Some(value) = &self.name { + write!(f, "{value} ")?; + } + write!(f, "ON {}", self.table_name)?; + if let Some(value) = &self.using { + write!(f, " USING {value} ")?; + } + write!(f, "({})", display_comma_separated(&self.columns))?; + if !self.include.is_empty() { + write!(f, " INCLUDE ({})", display_comma_separated(&self.include))?; + } + if let Some(value) = self.nulls_distinct { + if value { + write!(f, " NULLS DISTINCT")?; + } else { + write!(f, " NULLS NOT DISTINCT")?; + } + } + if !self.with.is_empty() { + write!(f, " WITH ({})", display_comma_separated(&self.with))?; + } + if let Some(predicate) = &self.predicate { + write!(f, " WHERE {predicate}")?; + } + if !self.index_options.is_empty() { + write!(f, " {}", display_separated(&self.index_options, " "))?; + } + if !self.alter_options.is_empty() { + write!(f, " {}", display_separated(&self.alter_options, " "))?; + } + Ok(()) + } +} + +/// CREATE TABLE statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateTable { + pub or_replace: bool, + pub temporary: bool, + pub external: bool, + pub dynamic: bool, + pub global: Option, + pub if_not_exists: bool, + pub transient: bool, + pub volatile: bool, + pub iceberg: bool, + /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub name: ObjectName, + /// Optional schema + pub columns: Vec, + pub constraints: Vec, + pub hive_distribution: HiveDistributionStyle, + pub hive_formats: Option, + pub table_options: CreateTableOptions, + pub file_format: Option, + pub location: Option, + pub query: Option>, + pub without_rowid: bool, + pub like: Option, + pub clone: Option, + pub version: Option, + // For Hive dialect, the table comment is after the column definitions without `=`, + // so the `comment` field is optional and different than the comment field in the general options list. + // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) + pub comment: Option, + pub on_commit: Option, + /// ClickHouse "ON CLUSTER" clause: + /// + pub on_cluster: Option, + /// ClickHouse "PRIMARY KEY " clause. + /// + pub primary_key: Option>, + /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different + /// than empty (represented as ()), the latter meaning "no sorting". + /// + pub order_by: Option>, + /// BigQuery: A partition expression for the table. + /// + pub partition_by: Option>, + /// BigQuery: Table clustering column list. + /// + /// Snowflake: Table clustering list which contains base column, expressions on base columns. + /// + pub cluster_by: Option>>, + /// Hive: Table clustering column list. + /// + pub clustered_by: Option, + /// Postgres `INHERITs` clause, which contains the list of tables from which + /// the new table inherits. + /// + /// + pub inherits: Option>, + /// SQLite "STRICT" clause. + /// if the "STRICT" table-option keyword is added to the end, after the closing ")", + /// then strict typing rules apply to that table. + pub strict: bool, + /// Snowflake "COPY GRANTS" clause + /// + pub copy_grants: bool, + /// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause + /// + pub enable_schema_evolution: Option, + /// Snowflake "CHANGE_TRACKING" clause + /// + pub change_tracking: Option, + /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause + /// + pub data_retention_time_in_days: Option, + /// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause + /// + pub max_data_extension_time_in_days: Option, + /// Snowflake "DEFAULT_DDL_COLLATION" clause + /// + pub default_ddl_collation: Option, + /// Snowflake "WITH AGGREGATION POLICY" clause + /// + pub with_aggregation_policy: Option, + /// Snowflake "WITH ROW ACCESS POLICY" clause + /// + pub with_row_access_policy: Option, + /// Snowflake "WITH TAG" clause + /// + pub with_tags: Option>, + /// Snowflake "EXTERNAL_VOLUME" clause for Iceberg tables + /// + pub external_volume: Option, + /// Snowflake "BASE_LOCATION" clause for Iceberg tables + /// + pub base_location: Option, + /// Snowflake "CATALOG" clause for Iceberg tables + /// + pub catalog: Option, + /// Snowflake "CATALOG_SYNC" clause for Iceberg tables + /// + pub catalog_sync: Option, + /// Snowflake "STORAGE_SERIALIZATION_POLICY" clause for Iceberg tables + /// + pub storage_serialization_policy: Option, + /// Snowflake "TARGET_LAG" clause for dybamic tables + /// + pub target_lag: Option, + /// Snowflake "WAREHOUSE" clause for dybamic tables + /// + pub warehouse: Option, + /// Snowflake "REFRESH_MODE" clause for dybamic tables + /// + pub refresh_mode: Option, + /// Snowflake "INITIALIZE" clause for dybamic tables + /// + pub initialize: Option, + /// Snowflake "REQUIRE USER" clause for dybamic tables + /// + pub require_user: bool, +} + +impl fmt::Display for CreateTable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // 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 {or_replace}{external}{global}{temporary}{transient}{volatile}{dynamic}{iceberg}TABLE {if_not_exists}{name}", + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + external = if self.external { "EXTERNAL " } else { "" }, + global = self.global + .map(|global| { + if global { + "GLOBAL " + } else { + "LOCAL " + } + }) + .unwrap_or(""), + if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + temporary = if self.temporary { "TEMPORARY " } else { "" }, + transient = if self.transient { "TRANSIENT " } else { "" }, + volatile = if self.volatile { "VOLATILE " } else { "" }, + // Only for Snowflake + iceberg = if self.iceberg { "ICEBERG " } else { "" }, + dynamic = if self.dynamic { "DYNAMIC " } else { "" }, + name = self.name, + )?; + if let Some(on_cluster) = &self.on_cluster { + write!(f, " ON CLUSTER {on_cluster}")?; + } + if !self.columns.is_empty() || !self.constraints.is_empty() { + f.write_str(" (")?; + NewLine.fmt(f)?; + Indent(DisplayCommaSeparated(&self.columns)).fmt(f)?; + if !self.columns.is_empty() && !self.constraints.is_empty() { + f.write_str(",")?; + SpaceOrNewline.fmt(f)?; + } + Indent(DisplayCommaSeparated(&self.constraints)).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")")?; + } else if self.query.is_none() && self.like.is_none() && self.clone.is_none() { + // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens + f.write_str(" ()")?; + } else if let Some(CreateTableLikeKind::Parenthesized(like_in_columns_list)) = &self.like { + write!(f, " ({like_in_columns_list})")?; + } + + // Hive table comment should be after column definitions, please refer to: + // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) + if let Some(comment) = &self.comment { + write!(f, " COMMENT '{comment}'")?; + } + + // Only for SQLite + if self.without_rowid { + write!(f, " WITHOUT ROWID")?; + } + + if let Some(CreateTableLikeKind::Plain(like)) = &self.like { + write!(f, " {like}")?; + } + + if let Some(c) = &self.clone { + write!(f, " CLONE {c}")?; + } + + if let Some(version) = &self.version { + write!(f, " {version}")?; + } + + match &self.hive_distribution { + HiveDistributionStyle::PARTITIONED { columns } => { + write!(f, " PARTITIONED BY ({})", display_comma_separated(columns))?; + } + 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(clustered_by) = &self.clustered_by { + write!(f, " {clustered_by}")?; + } + + if let Some(HiveFormat { + row_format, + serde_properties, + storage, + location, + }) = &self.hive_formats + { + match row_format { + Some(HiveRowFormat::SERDE { class }) => write!(f, " ROW FORMAT SERDE '{class}'")?, + Some(HiveRowFormat::DELIMITED { delimiters }) => { + write!(f, " ROW FORMAT DELIMITED")?; + if !delimiters.is_empty() { + write!(f, " {}", display_separated(delimiters, " "))?; + } + } + None => (), + } + match storage { + Some(HiveIOFormat::IOF { + input_format, + output_format, + }) => write!( + f, + " STORED AS INPUTFORMAT {input_format} OUTPUTFORMAT {output_format}" + )?, + Some(HiveIOFormat::FileFormat { format }) if !self.external => { + write!(f, " STORED AS {format}")? + } + _ => (), + } + if let Some(serde_properties) = serde_properties.as_ref() { + write!( + f, + " WITH SERDEPROPERTIES ({})", + display_comma_separated(serde_properties) + )?; + } + if !self.external { + if let Some(loc) = location { + write!(f, " LOCATION '{loc}'")?; + } + } + } + if self.external { + if let Some(file_format) = self.file_format { + write!(f, " STORED AS {file_format}")?; + } + write!(f, " LOCATION '{}'", self.location.as_ref().unwrap())?; + } + + match &self.table_options { + options @ CreateTableOptions::With(_) + | options @ CreateTableOptions::Plain(_) + | options @ CreateTableOptions::TableProperties(_) => write!(f, " {options}")?, + _ => (), + } + + if let Some(primary_key) = &self.primary_key { + write!(f, " PRIMARY KEY {primary_key}")?; + } + if let Some(order_by) = &self.order_by { + write!(f, " ORDER BY {order_by}")?; + } + if let Some(inherits) = &self.inherits { + write!(f, " INHERITS ({})", display_comma_separated(inherits))?; + } + if let Some(partition_by) = self.partition_by.as_ref() { + write!(f, " PARTITION BY {partition_by}")?; + } + if let Some(cluster_by) = self.cluster_by.as_ref() { + write!(f, " CLUSTER BY {cluster_by}")?; + } + if let options @ CreateTableOptions::Options(_) = &self.table_options { + write!(f, " {options}")?; + } + if let Some(external_volume) = self.external_volume.as_ref() { + write!(f, " EXTERNAL_VOLUME='{external_volume}'")?; + } + + if let Some(catalog) = self.catalog.as_ref() { + write!(f, " CATALOG='{catalog}'")?; + } + + if self.iceberg { + if let Some(base_location) = self.base_location.as_ref() { + write!(f, " BASE_LOCATION='{base_location}'")?; + } + } + + if let Some(catalog_sync) = self.catalog_sync.as_ref() { + write!(f, " CATALOG_SYNC='{catalog_sync}'")?; + } + + if let Some(storage_serialization_policy) = self.storage_serialization_policy.as_ref() { + write!( + f, + " STORAGE_SERIALIZATION_POLICY={storage_serialization_policy}" + )?; + } + + if self.copy_grants { + write!(f, " COPY GRANTS")?; + } + + if let Some(is_enabled) = self.enable_schema_evolution { + write!( + f, + " ENABLE_SCHEMA_EVOLUTION={}", + if is_enabled { "TRUE" } else { "FALSE" } + )?; + } + + if let Some(is_enabled) = self.change_tracking { + write!( + f, + " CHANGE_TRACKING={}", + if is_enabled { "TRUE" } else { "FALSE" } + )?; + } + + if let Some(data_retention_time_in_days) = self.data_retention_time_in_days { + write!( + f, + " DATA_RETENTION_TIME_IN_DAYS={data_retention_time_in_days}", + )?; + } + + if let Some(max_data_extension_time_in_days) = self.max_data_extension_time_in_days { + write!( + f, + " MAX_DATA_EXTENSION_TIME_IN_DAYS={max_data_extension_time_in_days}", + )?; + } + + if let Some(default_ddl_collation) = &self.default_ddl_collation { + write!(f, " DEFAULT_DDL_COLLATION='{default_ddl_collation}'",)?; + } + + if let Some(with_aggregation_policy) = &self.with_aggregation_policy { + write!(f, " WITH AGGREGATION POLICY {with_aggregation_policy}",)?; + } + + if let Some(row_access_policy) = &self.with_row_access_policy { + write!(f, " {row_access_policy}",)?; + } + + if let Some(tag) = &self.with_tags { + write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?; + } + + if let Some(target_lag) = &self.target_lag { + write!(f, " TARGET_LAG='{target_lag}'")?; + } + + if let Some(warehouse) = &self.warehouse { + write!(f, " WAREHOUSE={warehouse}")?; + } + + if let Some(refresh_mode) = &self.refresh_mode { + write!(f, " REFRESH_MODE={refresh_mode}")?; + } + + if let Some(initialize) = &self.initialize { + write!(f, " INITIALIZE={initialize}")?; + } + + if self.require_user { + write!(f, " REQUIRE USER")?; + } + + if self.on_commit.is_some() { + let on_commit = match self.on_commit { + Some(OnCommit::DeleteRows) => "ON COMMIT DELETE ROWS", + Some(OnCommit::PreserveRows) => "ON COMMIT PRESERVE ROWS", + Some(OnCommit::Drop) => "ON COMMIT DROP", + None => "", + }; + write!(f, " {on_commit}")?; + } + if self.strict { + write!(f, " STRICT")?; + } + if let Some(query) = &self.query { + write!(f, " AS {query}")?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// ```sql +/// CREATE DOMAIN name [ AS ] data_type +/// [ COLLATE collation ] +/// [ DEFAULT expression ] +/// [ domain_constraint [ ... ] ] +/// +/// where domain_constraint is: +/// +/// [ CONSTRAINT constraint_name ] +/// { NOT NULL | NULL | CHECK (expression) } +/// ``` +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createdomain.html) +pub struct CreateDomain { + /// The name of the domain to be created. + pub name: ObjectName, + /// The data type of the domain. + pub data_type: DataType, + /// The collation of the domain. + pub collation: Option, + /// The default value of the domain. + pub default: Option, + /// The constraints of the domain. + pub constraints: Vec, +} + +impl fmt::Display for CreateDomain { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE DOMAIN {name} AS {data_type}", + name = self.name, + data_type = self.data_type + )?; + if let Some(collation) = &self.collation { + write!(f, " COLLATE {collation}")?; + } + if let Some(default) = &self.default { + write!(f, " DEFAULT {default}")?; + } + if !self.constraints.is_empty() { + write!(f, " {}", display_separated(&self.constraints, " "))?; + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2264,6 +2996,12 @@ impl fmt::Display for CreateFunction { if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { write!(f, " RETURN {function_body}")?; } + if let Some(CreateFunctionBody::AsReturnExpr(function_body)) = &self.function_body { + write!(f, " AS RETURN {function_body}")?; + } + if let Some(CreateFunctionBody::AsReturnSelect(function_body)) = &self.function_body { + write!(f, " AS RETURN {function_body}")?; + } if let Some(using) = &self.using { write!(f, " {using}")?; } @@ -2341,3 +3079,100 @@ impl fmt::Display for CreateConnector { Ok(()) } } + +/// An `ALTER SCHEMA` (`Statement::AlterSchema`) operation. +/// +/// See [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#alter_schema_collate_statement) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterSchemaOperation { + SetDefaultCollate { + collate: Expr, + }, + AddReplica { + replica: Ident, + options: Option>, + }, + DropReplica { + replica: Ident, + }, + SetOptionsParens { + options: Vec, + }, +} + +impl fmt::Display for AlterSchemaOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterSchemaOperation::SetDefaultCollate { collate } => { + write!(f, "SET DEFAULT COLLATE {collate}") + } + AlterSchemaOperation::AddReplica { replica, options } => { + write!(f, "ADD REPLICA {replica}")?; + if let Some(options) = options { + write!(f, " OPTIONS ({})", display_comma_separated(options))?; + } + Ok(()) + } + AlterSchemaOperation::DropReplica { replica } => write!(f, "DROP REPLICA {replica}"), + AlterSchemaOperation::SetOptionsParens { options } => { + write!(f, "SET OPTIONS ({})", display_comma_separated(options)) + } + } + } +} +/// `RenameTableNameKind` is the kind used in an `ALTER TABLE _ RENAME` statement. +/// +/// Note: [MySQL] is the only database that supports the AS keyword for this operation. +/// +/// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RenameTableNameKind { + As(ObjectName), + To(ObjectName), +} + +impl fmt::Display for RenameTableNameKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RenameTableNameKind::As(name) => write!(f, "AS {name}"), + RenameTableNameKind::To(name) => write!(f, "TO {name}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterSchema { + pub name: ObjectName, + pub if_exists: bool, + pub operations: Vec, +} + +impl fmt::Display for AlterSchema { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ALTER SCHEMA ")?; + if self.if_exists { + write!(f, "IF EXISTS ")?; + } + write!(f, "{}", self.name)?; + for operation in &self.operations { + write!(f, " {operation}")?; + } + + Ok(()) + } +} + +impl Spanned for RenameTableNameKind { + fn span(&self) -> Span { + match self { + RenameTableNameKind::As(name) => name.span(), + RenameTableNameKind::To(name) => name.span(), + } + } +} diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 9cdb1ca86..63d6b86c7 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -29,506 +29,14 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -pub use super::ddl::{ColumnDef, TableConstraint}; +use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; use super::{ - display_comma_separated, display_separated, query::InputFormatClause, Assignment, ClusteredBy, - CommentDef, Expr, FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, - HiveRowFormat, Ident, IndexType, InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, - OnInsert, OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, Setting, - SqlOption, SqliteOnConflict, StorageSerializationPolicy, TableEngine, TableObject, - TableWithJoins, Tag, WrappedCollection, + display_comma_separated, query::InputFormatClause, Assignment, Expr, FromTable, Ident, + InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, Query, SelectItem, + Setting, SqliteOnConflict, TableObject, TableWithJoins, }; -/// Index column type. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct IndexColumn { - pub column: OrderByExpr, - pub operator_class: Option, -} - -impl Display for IndexColumn { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.column)?; - if let Some(operator_class) = &self.operator_class { - write!(f, " {}", operator_class)?; - } - Ok(()) - } -} - -/// CREATE INDEX statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct CreateIndex { - /// index name - pub name: Option, - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - pub table_name: ObjectName, - pub using: Option, - pub columns: Vec, - pub unique: bool, - pub concurrently: bool, - pub if_not_exists: bool, - pub include: Vec, - pub nulls_distinct: Option, - /// WITH clause: - pub with: Vec, - pub predicate: Option, -} - -impl Display for CreateIndex { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "CREATE {unique}INDEX {concurrently}{if_not_exists}", - unique = if self.unique { "UNIQUE " } else { "" }, - concurrently = if self.concurrently { - "CONCURRENTLY " - } else { - "" - }, - if_not_exists = if self.if_not_exists { - "IF NOT EXISTS " - } else { - "" - }, - )?; - if let Some(value) = &self.name { - write!(f, "{value} ")?; - } - write!(f, "ON {}", self.table_name)?; - if let Some(value) = &self.using { - write!(f, " USING {value} ")?; - } - write!(f, "({})", display_separated(&self.columns, ","))?; - if !self.include.is_empty() { - write!(f, " INCLUDE ({})", display_separated(&self.include, ","))?; - } - if let Some(value) = self.nulls_distinct { - if value { - write!(f, " NULLS DISTINCT")?; - } else { - write!(f, " NULLS NOT DISTINCT")?; - } - } - if !self.with.is_empty() { - write!(f, " WITH ({})", display_comma_separated(&self.with))?; - } - if let Some(predicate) = &self.predicate { - write!(f, " WHERE {predicate}")?; - } - Ok(()) - } -} - -/// CREATE TABLE statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct CreateTable { - pub or_replace: bool, - pub temporary: bool, - pub external: bool, - pub global: Option, - pub if_not_exists: bool, - pub transient: bool, - pub volatile: bool, - pub iceberg: bool, - /// Table name - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - pub name: ObjectName, - /// Optional schema - pub columns: Vec, - pub constraints: Vec, - pub hive_distribution: HiveDistributionStyle, - pub hive_formats: Option, - pub table_properties: Vec, - pub with_options: Vec, - pub file_format: Option, - pub location: Option, - pub query: Option>, - pub without_rowid: bool, - pub like: Option, - pub clone: Option, - pub engine: Option, - pub comment: Option, - pub auto_increment_offset: Option, - pub default_charset: Option, - pub collation: Option, - pub on_commit: Option, - /// ClickHouse "ON CLUSTER" clause: - /// - pub on_cluster: Option, - /// ClickHouse "PRIMARY KEY " clause. - /// - pub primary_key: Option>, - /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different - /// than empty (represented as ()), the latter meaning "no sorting". - /// - pub order_by: Option>, - /// BigQuery: A partition expression for the table. - /// - pub partition_by: Option>, - /// BigQuery: Table clustering column list. - /// - pub cluster_by: Option>>, - /// Hive: Table clustering column list. - /// - pub clustered_by: Option, - /// BigQuery: Table options list. - /// - pub options: Option>, - /// Postgres `INHERITs` clause, which contains the list of tables from which - /// the new table inherits. - /// - /// - pub inherits: Option>, - /// SQLite "STRICT" clause. - /// if the "STRICT" table-option keyword is added to the end, after the closing ")", - /// then strict typing rules apply to that table. - pub strict: bool, - /// Snowflake "COPY GRANTS" clause - /// - pub copy_grants: bool, - /// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause - /// - pub enable_schema_evolution: Option, - /// Snowflake "CHANGE_TRACKING" clause - /// - pub change_tracking: Option, - /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause - /// - pub data_retention_time_in_days: Option, - /// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause - /// - pub max_data_extension_time_in_days: Option, - /// Snowflake "DEFAULT_DDL_COLLATION" clause - /// - pub default_ddl_collation: Option, - /// Snowflake "WITH AGGREGATION POLICY" clause - /// - pub with_aggregation_policy: Option, - /// Snowflake "WITH ROW ACCESS POLICY" clause - /// - pub with_row_access_policy: Option, - /// Snowflake "WITH TAG" clause - /// - pub with_tags: Option>, - /// Snowflake "EXTERNAL_VOLUME" clause for Iceberg tables - /// - pub external_volume: Option, - /// Snowflake "BASE_LOCATION" clause for Iceberg tables - /// - pub base_location: Option, - /// Snowflake "CATALOG" clause for Iceberg tables - /// - pub catalog: Option, - /// Snowflake "CATALOG_SYNC" clause for Iceberg tables - /// - pub catalog_sync: Option, - /// Snowflake "STORAGE_SERIALIZATION_POLICY" clause for Iceberg tables - /// - pub storage_serialization_policy: Option, -} - -impl Display for CreateTable { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // 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 {or_replace}{external}{global}{temporary}{transient}{volatile}{iceberg}TABLE {if_not_exists}{name}", - or_replace = if self.or_replace { "OR REPLACE " } else { "" }, - external = if self.external { "EXTERNAL " } else { "" }, - global = self.global - .map(|global| { - if global { - "GLOBAL " - } else { - "LOCAL " - } - }) - .unwrap_or(""), - if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, - temporary = if self.temporary { "TEMPORARY " } else { "" }, - transient = if self.transient { "TRANSIENT " } else { "" }, - volatile = if self.volatile { "VOLATILE " } else { "" }, - // Only for Snowflake - iceberg = if self.iceberg { "ICEBERG " } else { "" }, - name = self.name, - )?; - if let Some(on_cluster) = &self.on_cluster { - write!(f, " ON CLUSTER {}", on_cluster)?; - } - if !self.columns.is_empty() || !self.constraints.is_empty() { - write!(f, " ({}", display_comma_separated(&self.columns))?; - if !self.columns.is_empty() && !self.constraints.is_empty() { - write!(f, ", ")?; - } - write!(f, "{})", display_comma_separated(&self.constraints))?; - } else if self.query.is_none() && self.like.is_none() && self.clone.is_none() { - // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens - write!(f, " ()")?; - } - - // Hive table comment should be after column definitions, please refer to: - // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) - if let Some(CommentDef::AfterColumnDefsWithoutEq(comment)) = &self.comment { - write!(f, " COMMENT '{comment}'")?; - } - - // Only for SQLite - if self.without_rowid { - write!(f, " WITHOUT ROWID")?; - } - - // Only for Hive - if let Some(l) = &self.like { - write!(f, " LIKE {l}")?; - } - - if let Some(c) = &self.clone { - write!(f, " CLONE {c}")?; - } - - match &self.hive_distribution { - HiveDistributionStyle::PARTITIONED { columns } => { - write!(f, " PARTITIONED BY ({})", display_comma_separated(columns))?; - } - 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(clustered_by) = &self.clustered_by { - write!(f, " {clustered_by}")?; - } - - if let Some(HiveFormat { - row_format, - serde_properties, - storage, - location, - }) = &self.hive_formats - { - match row_format { - Some(HiveRowFormat::SERDE { class }) => write!(f, " ROW FORMAT SERDE '{class}'")?, - Some(HiveRowFormat::DELIMITED { delimiters }) => { - write!(f, " ROW FORMAT DELIMITED")?; - if !delimiters.is_empty() { - write!(f, " {}", display_separated(delimiters, " "))?; - } - } - None => (), - } - match storage { - Some(HiveIOFormat::IOF { - input_format, - output_format, - }) => write!( - f, - " STORED AS INPUTFORMAT {input_format} OUTPUTFORMAT {output_format}" - )?, - Some(HiveIOFormat::FileFormat { format }) if !self.external => { - write!(f, " STORED AS {format}")? - } - _ => (), - } - if let Some(serde_properties) = serde_properties.as_ref() { - write!( - f, - " WITH SERDEPROPERTIES ({})", - display_comma_separated(serde_properties) - )?; - } - if !self.external { - if let Some(loc) = location { - write!(f, " LOCATION '{loc}'")?; - } - } - } - if self.external { - if let Some(file_format) = self.file_format { - write!(f, " STORED AS {file_format}")?; - } - write!(f, " LOCATION '{}'", self.location.as_ref().unwrap())?; - } - if !self.table_properties.is_empty() { - write!( - f, - " TBLPROPERTIES ({})", - display_comma_separated(&self.table_properties) - )?; - } - if !self.with_options.is_empty() { - write!(f, " WITH ({})", display_comma_separated(&self.with_options))?; - } - if let Some(engine) = &self.engine { - write!(f, " ENGINE={engine}")?; - } - if let Some(comment_def) = &self.comment { - match comment_def { - CommentDef::WithEq(comment) => { - write!(f, " COMMENT = '{comment}'")?; - } - CommentDef::WithoutEq(comment) => { - write!(f, " COMMENT '{comment}'")?; - } - // For CommentDef::AfterColumnDefsWithoutEq will be displayed after column definition - CommentDef::AfterColumnDefsWithoutEq(_) => (), - } - } - - if let Some(auto_increment_offset) = self.auto_increment_offset { - write!(f, " AUTO_INCREMENT {auto_increment_offset}")?; - } - if let Some(primary_key) = &self.primary_key { - write!(f, " PRIMARY KEY {}", primary_key)?; - } - if let Some(order_by) = &self.order_by { - write!(f, " ORDER BY {}", order_by)?; - } - if let Some(inherits) = &self.inherits { - write!(f, " INHERITS ({})", display_comma_separated(inherits))?; - } - if let Some(partition_by) = self.partition_by.as_ref() { - write!(f, " PARTITION BY {partition_by}")?; - } - if let Some(cluster_by) = self.cluster_by.as_ref() { - write!(f, " CLUSTER BY {cluster_by}")?; - } - - if let Some(options) = self.options.as_ref() { - write!( - f, - " OPTIONS({})", - display_comma_separated(options.as_slice()) - )?; - } - - if let Some(external_volume) = self.external_volume.as_ref() { - write!(f, " EXTERNAL_VOLUME = '{external_volume}'")?; - } - - if let Some(catalog) = self.catalog.as_ref() { - write!(f, " CATALOG = '{catalog}'")?; - } - - if self.iceberg { - if let Some(base_location) = self.base_location.as_ref() { - write!(f, " BASE_LOCATION = '{base_location}'")?; - } - } - - if let Some(catalog_sync) = self.catalog_sync.as_ref() { - write!(f, " CATALOG_SYNC = '{catalog_sync}'")?; - } - - if let Some(storage_serialization_policy) = self.storage_serialization_policy.as_ref() { - write!( - f, - " STORAGE_SERIALIZATION_POLICY = {storage_serialization_policy}" - )?; - } - - if self.copy_grants { - write!(f, " COPY GRANTS")?; - } - - if let Some(is_enabled) = self.enable_schema_evolution { - write!( - f, - " ENABLE_SCHEMA_EVOLUTION={}", - if is_enabled { "TRUE" } else { "FALSE" } - )?; - } - - if let Some(is_enabled) = self.change_tracking { - write!( - f, - " CHANGE_TRACKING={}", - if is_enabled { "TRUE" } else { "FALSE" } - )?; - } - - if let Some(data_retention_time_in_days) = self.data_retention_time_in_days { - write!( - f, - " DATA_RETENTION_TIME_IN_DAYS={data_retention_time_in_days}", - )?; - } - - if let Some(max_data_extension_time_in_days) = self.max_data_extension_time_in_days { - write!( - f, - " MAX_DATA_EXTENSION_TIME_IN_DAYS={max_data_extension_time_in_days}", - )?; - } - - if let Some(default_ddl_collation) = &self.default_ddl_collation { - write!(f, " DEFAULT_DDL_COLLATION='{default_ddl_collation}'",)?; - } - - if let Some(with_aggregation_policy) = &self.with_aggregation_policy { - write!(f, " WITH AGGREGATION POLICY {with_aggregation_policy}",)?; - } - - if let Some(row_access_policy) = &self.with_row_access_policy { - write!(f, " {row_access_policy}",)?; - } - - if let Some(tag) = &self.with_tags { - write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?; - } - - if let Some(default_charset) = &self.default_charset { - write!(f, " DEFAULT CHARSET={default_charset}")?; - } - if let Some(collation) = &self.collation { - write!(f, " COLLATE={collation}")?; - } - - if self.on_commit.is_some() { - let on_commit = match self.on_commit { - Some(OnCommit::DeleteRows) => "ON COMMIT DELETE ROWS", - Some(OnCommit::PreserveRows) => "ON COMMIT PRESERVE ROWS", - Some(OnCommit::Drop) => "ON COMMIT DROP", - None => "", - }; - write!(f, " {on_commit}")?; - } - if self.strict { - write!(f, " STRICT")?; - } - if let Some(query) = &self.query { - write!(f, " AS {query}")?; - } - Ok(()) - } -} - /// INSERT statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -618,28 +126,32 @@ impl Display for Insert { )?; } if !self.columns.is_empty() { - write!(f, "({}) ", display_comma_separated(&self.columns))?; + write!(f, "({})", display_comma_separated(&self.columns))?; + SpaceOrNewline.fmt(f)?; } if let Some(ref parts) = self.partitioned { if !parts.is_empty() { - write!(f, "PARTITION ({}) ", display_comma_separated(parts))?; + write!(f, "PARTITION ({})", display_comma_separated(parts))?; + SpaceOrNewline.fmt(f)?; } } if !self.after_columns.is_empty() { - write!(f, "({}) ", display_comma_separated(&self.after_columns))?; + write!(f, "({})", display_comma_separated(&self.after_columns))?; + SpaceOrNewline.fmt(f)?; } if let Some(settings) = &self.settings { - write!(f, "SETTINGS {} ", display_comma_separated(settings))?; + write!(f, "SETTINGS {}", display_comma_separated(settings))?; + SpaceOrNewline.fmt(f)?; } if let Some(source) = &self.source { - write!(f, "{source}")?; + source.fmt(f)?; } else if !self.assignments.is_empty() { - write!(f, "SET ")?; - write!(f, "{}", display_comma_separated(&self.assignments))?; + write!(f, "SET")?; + indented_list(f, &self.assignments)?; } else if let Some(format_clause) = &self.format_clause { - write!(f, "{format_clause}")?; + format_clause.fmt(f)?; } else if self.columns.is_empty() { write!(f, "DEFAULT VALUES")?; } @@ -659,7 +171,9 @@ impl Display for Insert { } if let Some(returning) = &self.returning { - write!(f, " RETURNING {}", display_comma_separated(returning))?; + SpaceOrNewline.fmt(f)?; + f.write_str("RETURNING")?; + indented_list(f, returning)?; } Ok(()) } @@ -688,32 +202,45 @@ pub struct Delete { impl Display for Delete { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "DELETE ")?; + f.write_str("DELETE")?; if !self.tables.is_empty() { - write!(f, "{} ", display_comma_separated(&self.tables))?; + indented_list(f, &self.tables)?; } match &self.from { FromTable::WithFromKeyword(from) => { - write!(f, "FROM {}", display_comma_separated(from))?; + f.write_str(" FROM")?; + indented_list(f, from)?; } FromTable::WithoutKeyword(from) => { - write!(f, "{}", display_comma_separated(from))?; + indented_list(f, from)?; } } if let Some(using) = &self.using { - write!(f, " USING {}", display_comma_separated(using))?; + SpaceOrNewline.fmt(f)?; + f.write_str("USING")?; + indented_list(f, using)?; } if let Some(selection) = &self.selection { - write!(f, " WHERE {selection}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("WHERE")?; + SpaceOrNewline.fmt(f)?; + Indent(selection).fmt(f)?; } if let Some(returning) = &self.returning { - write!(f, " RETURNING {}", display_comma_separated(returning))?; + SpaceOrNewline.fmt(f)?; + f.write_str("RETURNING")?; + indented_list(f, returning)?; } if !self.order_by.is_empty() { - write!(f, " ORDER BY {}", display_comma_separated(&self.order_by))?; + SpaceOrNewline.fmt(f)?; + f.write_str("ORDER BY")?; + indented_list(f, &self.order_by)?; } if let Some(limit) = &self.limit { - write!(f, " LIMIT {limit}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("LIMIT")?; + SpaceOrNewline.fmt(f)?; + Indent(limit).fmt(f)?; } Ok(()) } diff --git a/src/ast/helpers/key_value_options.rs b/src/ast/helpers/key_value_options.rs index 06f028dd2..7f1bb0fdb 100644 --- a/src/ast/helpers/key_value_options.rs +++ b/src/ast/helpers/key_value_options.rs @@ -31,11 +31,22 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +use crate::ast::display_separated; + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct KeyValueOptions { pub options: Vec, + pub delimiter: KeyValueOptionsDelimiter, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum KeyValueOptionsDelimiter { + Space, + Comma, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -59,18 +70,11 @@ pub struct KeyValueOption { impl fmt::Display for KeyValueOptions { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if !self.options.is_empty() { - let mut first = false; - for option in &self.options { - if !first { - first = true; - } else { - f.write_str(" ")?; - } - write!(f, "{}", option)?; - } - } - Ok(()) + let sep = match self.delimiter { + KeyValueOptionsDelimiter::Space => " ", + KeyValueOptionsDelimiter::Comma => ", ", + }; + write!(f, "{}", display_separated(&self.options, sep)) } } diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs index 55831220d..3efbcf7b0 100644 --- a/src/ast/helpers/mod.rs +++ b/src/ast/helpers/mod.rs @@ -16,5 +16,6 @@ // under the License. pub mod attached_token; pub mod key_value_options; +pub mod stmt_create_database; pub mod stmt_create_table; pub mod stmt_data_loading; diff --git a/src/ast/helpers/stmt_create_database.rs b/src/ast/helpers/stmt_create_database.rs new file mode 100644 index 000000000..94997bfa5 --- /dev/null +++ b/src/ast/helpers/stmt_create_database.rs @@ -0,0 +1,324 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#[cfg(not(feature = "std"))] +use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +use crate::ast::{ + CatalogSyncNamespaceMode, ContactEntry, ObjectName, Statement, StorageSerializationPolicy, Tag, +}; +use crate::parser::ParserError; + +/// Builder for create database statement variant ([1]). +/// +/// This structure helps building and accessing a create database with more ease, without needing to: +/// - Match the enum itself a lot of times; or +/// - Moving a lot of variables around the code. +/// +/// # Example +/// ```rust +/// use sqlparser::ast::helpers::stmt_create_database::CreateDatabaseBuilder; +/// use sqlparser::ast::{ColumnDef, Ident, ObjectName}; +/// let builder = CreateDatabaseBuilder::new(ObjectName::from(vec![Ident::new("database_name")])) +/// .if_not_exists(true); +/// // You can access internal elements with ease +/// assert!(builder.if_not_exists); +/// // Convert to a statement +/// assert_eq!( +/// builder.build().to_string(), +/// "CREATE DATABASE IF NOT EXISTS database_name" +/// ) +/// ``` +/// +/// [1]: Statement::CreateDatabase +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateDatabaseBuilder { + pub db_name: ObjectName, + pub if_not_exists: bool, + pub location: Option, + pub managed_location: Option, + pub or_replace: bool, + pub transient: bool, + pub clone: Option, + pub data_retention_time_in_days: Option, + pub max_data_extension_time_in_days: Option, + pub external_volume: Option, + pub catalog: Option, + pub replace_invalid_characters: Option, + pub default_ddl_collation: Option, + pub storage_serialization_policy: Option, + pub comment: Option, + pub catalog_sync: Option, + pub catalog_sync_namespace_mode: Option, + pub catalog_sync_namespace_flatten_delimiter: Option, + pub with_tags: Option>, + pub with_contacts: Option>, +} + +impl CreateDatabaseBuilder { + pub fn new(name: ObjectName) -> Self { + Self { + db_name: name, + if_not_exists: false, + location: None, + managed_location: None, + or_replace: false, + transient: false, + clone: None, + data_retention_time_in_days: None, + max_data_extension_time_in_days: None, + external_volume: None, + catalog: None, + replace_invalid_characters: None, + default_ddl_collation: None, + storage_serialization_policy: None, + comment: None, + catalog_sync: None, + catalog_sync_namespace_mode: None, + catalog_sync_namespace_flatten_delimiter: None, + with_tags: None, + with_contacts: None, + } + } + + pub fn location(mut self, location: Option) -> Self { + self.location = location; + self + } + + pub fn managed_location(mut self, managed_location: Option) -> Self { + self.managed_location = managed_location; + self + } + + pub fn or_replace(mut self, or_replace: bool) -> Self { + self.or_replace = or_replace; + self + } + + pub fn transient(mut self, transient: bool) -> Self { + self.transient = transient; + self + } + + pub fn if_not_exists(mut self, if_not_exists: bool) -> Self { + self.if_not_exists = if_not_exists; + self + } + + pub fn clone_clause(mut self, clone: Option) -> Self { + self.clone = clone; + self + } + + pub fn data_retention_time_in_days(mut self, data_retention_time_in_days: Option) -> Self { + self.data_retention_time_in_days = data_retention_time_in_days; + self + } + + pub fn max_data_extension_time_in_days( + mut self, + max_data_extension_time_in_days: Option, + ) -> Self { + self.max_data_extension_time_in_days = max_data_extension_time_in_days; + self + } + + pub fn external_volume(mut self, external_volume: Option) -> Self { + self.external_volume = external_volume; + self + } + + pub fn catalog(mut self, catalog: Option) -> Self { + self.catalog = catalog; + self + } + + pub fn replace_invalid_characters(mut self, replace_invalid_characters: Option) -> Self { + self.replace_invalid_characters = replace_invalid_characters; + self + } + + pub fn default_ddl_collation(mut self, default_ddl_collation: Option) -> Self { + self.default_ddl_collation = default_ddl_collation; + self + } + + pub fn storage_serialization_policy( + mut self, + storage_serialization_policy: Option, + ) -> Self { + self.storage_serialization_policy = storage_serialization_policy; + self + } + + pub fn comment(mut self, comment: Option) -> Self { + self.comment = comment; + self + } + + pub fn catalog_sync(mut self, catalog_sync: Option) -> Self { + self.catalog_sync = catalog_sync; + self + } + + pub fn catalog_sync_namespace_mode( + mut self, + catalog_sync_namespace_mode: Option, + ) -> Self { + self.catalog_sync_namespace_mode = catalog_sync_namespace_mode; + self + } + + pub fn catalog_sync_namespace_flatten_delimiter( + mut self, + catalog_sync_namespace_flatten_delimiter: Option, + ) -> Self { + self.catalog_sync_namespace_flatten_delimiter = catalog_sync_namespace_flatten_delimiter; + self + } + + pub fn with_tags(mut self, with_tags: Option>) -> Self { + self.with_tags = with_tags; + self + } + + pub fn with_contacts(mut self, with_contacts: Option>) -> Self { + self.with_contacts = with_contacts; + self + } + + pub fn build(self) -> Statement { + Statement::CreateDatabase { + db_name: self.db_name, + if_not_exists: self.if_not_exists, + managed_location: self.managed_location, + location: self.location, + or_replace: self.or_replace, + transient: self.transient, + clone: self.clone, + data_retention_time_in_days: self.data_retention_time_in_days, + max_data_extension_time_in_days: self.max_data_extension_time_in_days, + external_volume: self.external_volume, + catalog: self.catalog, + replace_invalid_characters: self.replace_invalid_characters, + default_ddl_collation: self.default_ddl_collation, + storage_serialization_policy: self.storage_serialization_policy, + comment: self.comment, + catalog_sync: self.catalog_sync, + catalog_sync_namespace_mode: self.catalog_sync_namespace_mode, + catalog_sync_namespace_flatten_delimiter: self.catalog_sync_namespace_flatten_delimiter, + with_tags: self.with_tags, + with_contacts: self.with_contacts, + } + } +} + +impl TryFrom for CreateDatabaseBuilder { + type Error = ParserError; + + fn try_from(stmt: Statement) -> Result { + match stmt { + Statement::CreateDatabase { + db_name, + if_not_exists, + location, + managed_location, + or_replace, + transient, + clone, + data_retention_time_in_days, + max_data_extension_time_in_days, + external_volume, + catalog, + replace_invalid_characters, + default_ddl_collation, + storage_serialization_policy, + comment, + catalog_sync, + catalog_sync_namespace_mode, + catalog_sync_namespace_flatten_delimiter, + with_tags, + with_contacts, + } => Ok(Self { + db_name, + if_not_exists, + location, + managed_location, + or_replace, + transient, + clone, + data_retention_time_in_days, + max_data_extension_time_in_days, + external_volume, + catalog, + replace_invalid_characters, + default_ddl_collation, + storage_serialization_policy, + comment, + catalog_sync, + catalog_sync_namespace_mode, + catalog_sync_namespace_flatten_delimiter, + with_tags, + with_contacts, + }), + _ => Err(ParserError::ParserError(format!( + "Expected create database statement, but received: {stmt}" + ))), + } + } +} + +#[cfg(test)] +mod tests { + use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; + use crate::ast::{Ident, ObjectName, Statement}; + use crate::parser::ParserError; + + #[test] + pub fn test_from_valid_statement() { + let builder = CreateDatabaseBuilder::new(ObjectName::from(vec![Ident::new("db_name")])); + + let stmt = builder.clone().build(); + + assert_eq!(builder, CreateDatabaseBuilder::try_from(stmt).unwrap()); + } + + #[test] + pub fn test_from_invalid_statement() { + let stmt = Statement::Commit { + chain: false, + end: false, + modifier: None, + }; + + assert_eq!( + CreateDatabaseBuilder::try_from(stmt).unwrap_err(), + ParserError::ParserError( + "Expected create database statement, but received: COMMIT".to_owned() + ) + ); + } +} diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 1c50cb843..fc0002697 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -24,12 +24,13 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use super::super::dml::CreateTable; use crate::ast::{ - ClusteredBy, ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, - ObjectName, OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, - StorageSerializationPolicy, TableConstraint, TableEngine, Tag, WrappedCollection, + ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions, Expr, + FileFormat, HiveDistributionStyle, HiveFormat, Ident, InitializeKind, ObjectName, OnCommit, + OneOrManyWithParens, Query, RefreshModeKind, RowAccessPolicy, Statement, + StorageSerializationPolicy, TableConstraint, TableVersion, Tag, WrappedCollection, }; + use crate::parser::ParserError; /// Builder for create table statement variant ([1]). @@ -71,32 +72,27 @@ pub struct CreateTableBuilder { pub transient: bool, pub volatile: bool, pub iceberg: bool, + pub dynamic: bool, pub name: ObjectName, pub columns: Vec, pub constraints: Vec, pub hive_distribution: HiveDistributionStyle, pub hive_formats: Option, - pub table_properties: Vec, - pub with_options: Vec, pub file_format: Option, pub location: Option, pub query: Option>, pub without_rowid: bool, - pub like: Option, + pub like: Option, pub clone: Option, - pub engine: Option, + pub version: Option, pub comment: Option, - pub auto_increment_offset: Option, - pub default_charset: Option, - pub collation: Option, pub on_commit: Option, pub on_cluster: Option, pub primary_key: Option>, pub order_by: Option>, pub partition_by: Option>, - pub cluster_by: Option>>, + pub cluster_by: Option>>, pub clustered_by: Option, - pub options: Option>, pub inherits: Option>, pub strict: bool, pub copy_grants: bool, @@ -113,6 +109,12 @@ pub struct CreateTableBuilder { pub catalog: Option, pub catalog_sync: Option, pub storage_serialization_policy: Option, + pub table_options: CreateTableOptions, + pub target_lag: Option, + pub warehouse: Option, + pub refresh_mode: Option, + pub initialize: Option, + pub require_user: bool, } impl CreateTableBuilder { @@ -126,24 +128,20 @@ impl CreateTableBuilder { transient: false, volatile: false, iceberg: false, + dynamic: false, name, columns: vec![], constraints: vec![], hive_distribution: HiveDistributionStyle::NONE, hive_formats: None, - table_properties: vec![], - with_options: vec![], file_format: None, location: None, query: None, without_rowid: false, like: None, clone: None, - engine: None, + version: None, comment: None, - auto_increment_offset: None, - default_charset: None, - collation: None, on_commit: None, on_cluster: None, primary_key: None, @@ -151,7 +149,6 @@ impl CreateTableBuilder { partition_by: None, cluster_by: None, clustered_by: None, - options: None, inherits: None, strict: false, copy_grants: false, @@ -168,6 +165,12 @@ impl CreateTableBuilder { catalog: None, catalog_sync: None, storage_serialization_policy: None, + table_options: CreateTableOptions::None, + target_lag: None, + warehouse: None, + refresh_mode: None, + initialize: None, + require_user: false, } } pub fn or_replace(mut self, or_replace: bool) -> Self { @@ -210,6 +213,11 @@ impl CreateTableBuilder { self } + pub fn dynamic(mut self, dynamic: bool) -> Self { + self.dynamic = dynamic; + self + } + pub fn columns(mut self, columns: Vec) -> Self { self.columns = columns; self @@ -230,15 +238,6 @@ impl CreateTableBuilder { self } - pub fn table_properties(mut self, table_properties: Vec) -> Self { - self.table_properties = table_properties; - self - } - - pub fn with_options(mut self, with_options: Vec) -> Self { - self.with_options = with_options; - self - } pub fn file_format(mut self, file_format: Option) -> Self { self.file_format = file_format; self @@ -257,7 +256,7 @@ impl CreateTableBuilder { self } - pub fn like(mut self, like: Option) -> Self { + pub fn like(mut self, like: Option) -> Self { self.like = like; self } @@ -268,31 +267,16 @@ impl CreateTableBuilder { self } - pub fn engine(mut self, engine: Option) -> Self { - self.engine = engine; + pub fn version(mut self, version: Option) -> Self { + self.version = version; self } - pub fn comment(mut self, comment: Option) -> Self { + pub fn comment_after_column_def(mut self, comment: Option) -> Self { self.comment = comment; self } - pub fn auto_increment_offset(mut self, offset: Option) -> Self { - self.auto_increment_offset = offset; - self - } - - pub fn default_charset(mut self, default_charset: Option) -> Self { - self.default_charset = default_charset; - self - } - - pub fn collation(mut self, collation: Option) -> Self { - self.collation = collation; - self - } - pub fn on_commit(mut self, on_commit: Option) -> Self { self.on_commit = on_commit; self @@ -318,7 +302,7 @@ impl CreateTableBuilder { self } - pub fn cluster_by(mut self, cluster_by: Option>>) -> Self { + pub fn cluster_by(mut self, cluster_by: Option>>) -> Self { self.cluster_by = cluster_by; self } @@ -328,11 +312,6 @@ impl CreateTableBuilder { self } - pub fn options(mut self, options: Option>) -> Self { - self.options = options; - self - } - pub fn inherits(mut self, inherits: Option>) -> Self { self.inherits = inherits; self @@ -422,6 +401,36 @@ impl CreateTableBuilder { self } + pub fn table_options(mut self, table_options: CreateTableOptions) -> Self { + self.table_options = table_options; + self + } + + pub fn target_lag(mut self, target_lag: Option) -> Self { + self.target_lag = target_lag; + self + } + + pub fn warehouse(mut self, warehouse: Option) -> Self { + self.warehouse = warehouse; + self + } + + pub fn refresh_mode(mut self, refresh_mode: Option) -> Self { + self.refresh_mode = refresh_mode; + self + } + + pub fn initialize(mut self, initialize: Option) -> Self { + self.initialize = initialize; + self + } + + pub fn require_user(mut self, require_user: bool) -> Self { + self.require_user = require_user; + self + } + pub fn build(self) -> Statement { Statement::CreateTable(CreateTable { or_replace: self.or_replace, @@ -432,24 +441,20 @@ impl CreateTableBuilder { transient: self.transient, volatile: self.volatile, iceberg: self.iceberg, + dynamic: self.dynamic, name: self.name, columns: self.columns, constraints: self.constraints, hive_distribution: self.hive_distribution, hive_formats: self.hive_formats, - table_properties: self.table_properties, - with_options: self.with_options, file_format: self.file_format, location: self.location, query: self.query, without_rowid: self.without_rowid, like: self.like, clone: self.clone, - engine: self.engine, + version: self.version, comment: self.comment, - auto_increment_offset: self.auto_increment_offset, - default_charset: self.default_charset, - collation: self.collation, on_commit: self.on_commit, on_cluster: self.on_cluster, primary_key: self.primary_key, @@ -457,7 +462,6 @@ impl CreateTableBuilder { partition_by: self.partition_by, cluster_by: self.cluster_by, clustered_by: self.clustered_by, - options: self.options, inherits: self.inherits, strict: self.strict, copy_grants: self.copy_grants, @@ -474,6 +478,12 @@ impl CreateTableBuilder { catalog: self.catalog, catalog_sync: self.catalog_sync, storage_serialization_policy: self.storage_serialization_policy, + table_options: self.table_options, + target_lag: self.target_lag, + warehouse: self.warehouse, + refresh_mode: self.refresh_mode, + initialize: self.initialize, + require_user: self.require_user, }) } } @@ -494,24 +504,20 @@ impl TryFrom for CreateTableBuilder { transient, volatile, iceberg, + dynamic, name, columns, constraints, hive_distribution, hive_formats, - table_properties, - with_options, file_format, location, query, without_rowid, like, clone, - engine, + version, comment, - auto_increment_offset, - default_charset, - collation, on_commit, on_cluster, primary_key, @@ -519,7 +525,6 @@ impl TryFrom for CreateTableBuilder { partition_by, cluster_by, clustered_by, - options, inherits, strict, copy_grants, @@ -536,6 +541,12 @@ impl TryFrom for CreateTableBuilder { catalog, catalog_sync, storage_serialization_policy, + table_options, + target_lag, + warehouse, + refresh_mode, + initialize, + require_user, }) => Ok(Self { or_replace, temporary, @@ -543,24 +554,20 @@ impl TryFrom for CreateTableBuilder { global, if_not_exists, transient, + dynamic, name, columns, constraints, hive_distribution, hive_formats, - table_properties, - with_options, file_format, location, query, without_rowid, like, clone, - engine, + version, comment, - auto_increment_offset, - default_charset, - collation, on_commit, on_cluster, primary_key, @@ -568,7 +575,6 @@ impl TryFrom for CreateTableBuilder { partition_by, cluster_by, clustered_by, - options, inherits, strict, iceberg, @@ -587,6 +593,12 @@ impl TryFrom for CreateTableBuilder { catalog, catalog_sync, storage_serialization_policy, + table_options, + target_lag, + warehouse, + refresh_mode, + initialize, + require_user, }), _ => Err(ParserError::ParserError(format!( "Expected create table statement, but received: {stmt}" @@ -599,9 +611,9 @@ impl TryFrom for CreateTableBuilder { #[derive(Default)] pub(crate) struct CreateTableConfiguration { pub partition_by: Option>, - pub cluster_by: Option>>, - pub options: Option>, + pub cluster_by: Option>>, pub inherits: Option>, + pub table_options: CreateTableOptions, } #[cfg(test)] diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index e960bb05b..92a727279 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -21,8 +21,6 @@ #[cfg(not(feature = "std"))] use alloc::string::String; -#[cfg(not(feature = "std"))] -use alloc::vec::Vec; use core::fmt; #[cfg(feature = "serde")] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d65889810..71cb6c5ee 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -28,6 +28,7 @@ use helpers::{ stmt_data_loading::{FileStagingCommand, StageLoadSelectItemKind}, }; +use core::cmp::Ordering; use core::ops::Deref; use core::{ fmt::{self, Display}, @@ -40,49 +41,57 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::keywords::Keyword; -use crate::tokenizer::{Span, Token}; +use crate::{ + display_utils::{indented_list, SpaceOrNewline}, + tokenizer::{Span, Token}, +}; +use crate::{ + display_utils::{Indent, NewLine}, + keywords::Keyword, +}; pub use self::data_type::{ ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember, - ExactNumberInfo, StructBracketKind, TimezoneInfo, + ExactNumberInfo, IntervalFields, StructBracketKind, TimezoneInfo, }; pub use self::dcl::{ AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, - AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterType, AlterTypeAddValue, - AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, - ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateConnector, CreateFunction, Deduplicate, DeferrableInitial, - DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexOption, - IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, ProcedureParam, - ReferentialAction, TableConstraint, TagsColumnOption, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeRepresentation, ViewColumnDef, + AlterSchema, AlterSchemaOperation, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, + AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, + AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, + ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, + CreateFunction, CreateIndex, CreateTable, Deduplicate, DeferrableInitial, DropBehavior, + GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, + IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, + IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, + ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TableConstraint, + TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ViewColumnDef, }; -pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert}; +pub use self::dml::{Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, - ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, Fetch, ForClause, ForJson, ForXml, - FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, - InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, - JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, - LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, - Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, - OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, PivotValueSource, - ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, - ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem, - SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Setting, - SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, - TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableSample, - TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, - TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, - TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, - WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition, XmlPassingArgument, - XmlPassingClause, XmlTableColumn, XmlTableColumnOption, + ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, ExprWithAliasAndOrderBy, Fetch, ForClause, + ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, + IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, + JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, + JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, + MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, + OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, + PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, + RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, + SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, SetOperator, + SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, + TableFunctionArgs, TableIndexHintForClause, TableIndexHintType, TableIndexHints, + TableIndexType, TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod, + TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, + TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, UpdateTableFromKind, + ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition, + XmlPassingArgument, XmlPassingClause, XmlTableColumn, XmlTableColumnOption, }; pub use self::trigger::{ @@ -134,9 +143,9 @@ where fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut delim = ""; for t in self.slice { - write!(f, "{delim}")?; + f.write_str(delim)?; delim = self.sep; - write!(f, "{t}")?; + t.fmt(f)?; } Ok(()) } @@ -166,7 +175,7 @@ fn format_statement_list(f: &mut fmt::Formatter, statements: &[Statement]) -> fm } /// An identifier, decomposed into its value or character data and the quote style. -#[derive(Debug, Clone, PartialOrd, Ord)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Ident { @@ -208,6 +217,35 @@ impl core::hash::Hash for Ident { impl Eq for Ident {} +impl PartialOrd for Ident { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Ident { + fn cmp(&self, other: &Self) -> Ordering { + let Ident { + value, + quote_style, + // exhaustiveness check; we ignore spans in ordering + span: _, + } = self; + + let Ident { + value: other_value, + quote_style: other_quote_style, + // exhaustiveness check; we ignore spans in ordering + span: _, + } = other; + + // First compare by value, then by quote_style + value + .cmp(other_value) + .then_with(|| quote_style.cmp(other_quote_style)) + } +} + impl Ident { /// Create a new identifier with the given value and no quotes and an empty span. pub fn new(value: S) -> Self @@ -307,12 +345,14 @@ impl fmt::Display for ObjectName { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ObjectNamePart { Identifier(Ident), + Function(ObjectNamePartFunction), } impl ObjectNamePart { pub fn as_ident(&self) -> Option<&Ident> { match self { ObjectNamePart::Identifier(ident) => Some(ident), + ObjectNamePart::Function(_) => None, } } } @@ -320,11 +360,31 @@ impl ObjectNamePart { impl fmt::Display for ObjectNamePart { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - ObjectNamePart::Identifier(ident) => write!(f, "{}", ident), + ObjectNamePart::Identifier(ident) => write!(f, "{ident}"), + ObjectNamePart::Function(func) => write!(f, "{func}"), } } } +/// An object name part that consists of a function that dynamically +/// constructs identifiers. +/// +/// - [Snowflake](https://docs.snowflake.com/en/sql-reference/identifier-literal) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ObjectNamePartFunction { + pub name: Ident, + pub args: Vec, +} + +impl fmt::Display for ObjectNamePartFunction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}(", self.name)?; + write!(f, "{})", display_comma_separated(&self.args)) + } +} + /// Represents an Array Expression, either /// `ARRAY[..]`, or `[..]` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -422,14 +482,22 @@ impl fmt::Display for Interval { pub struct StructField { pub field_name: Option, pub field_type: DataType, + /// Struct field options. + /// See [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#column_name_and_column_schema) + pub options: Option>, } impl fmt::Display for StructField { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(name) = &self.field_name { - write!(f, "{name} {}", self.field_type) + write!(f, "{name} {}", self.field_type)?; + } else { + write!(f, "{}", self.field_type)?; + } + if let Some(options) = &self.options { + write!(f, " OPTIONS({})", display_separated(options, ", ")) } else { - write!(f, "{}", self.field_type) + Ok(()) } } } @@ -628,7 +696,12 @@ pub struct CaseWhen { impl fmt::Display for CaseWhen { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "WHEN {} THEN {}", self.condition, self.result) + f.write_str("WHEN ")?; + self.condition.fmt(f)?; + f.write_str(" THEN")?; + SpaceOrNewline.fmt(f)?; + Indent(&self.result).fmt(f)?; + Ok(()) } } @@ -729,7 +802,7 @@ pub enum Expr { /// `[ NOT ] IN (SELECT ...)` InSubquery { expr: Box, - subquery: Box, + subquery: Box, negated: bool, }, /// `[ NOT ] IN UNNEST(array_expression)` @@ -759,7 +832,7 @@ pub enum Expr { any: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// `ILIKE` (case-insensitive `LIKE`) ILike { @@ -769,14 +842,14 @@ pub enum Expr { any: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// SIMILAR TO regex SimilarTo { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// MySQL: RLIKE regex or REGEXP regex RLike { @@ -942,12 +1015,7 @@ pub enum Expr { /// 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, - /// The value of the constant. - /// Hint: you can unwrap the string value using `value.into_string()`. - value: Value, - }, + TypedString(TypedString), /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), /// `CASE [] WHEN THEN ... [ELSE ] END` @@ -956,6 +1024,8 @@ pub enum Expr { /// not `< 0` nor `1, 2, 3` as allowed in a `` per /// Case { + case_token: AttachedToken, + end_token: AttachedToken, operand: Option>, conditions: Vec, else_result: Option>, @@ -1070,8 +1140,10 @@ pub enum Expr { /// /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/functions#higher-order-functions---operator-and-lambdaparams-expr-function) /// [Databricks](https://docs.databricks.com/en/sql/language-manual/sql-ref-lambda-functions.html) - /// [DuckDb](https://duckdb.org/docs/sql/functions/lambda.html) + /// [DuckDB](https://duckdb.org/docs/stable/sql/functions/lambda) Lambda(LambdaFunction), + /// Checks membership of a value in a JSON array + MemberOf(MemberOf), } impl Expr { @@ -1158,8 +1230,8 @@ pub enum AccessExpr { impl fmt::Display for AccessExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - AccessExpr::Dot(expr) => write!(f, ".{}", expr), - AccessExpr::Subscript(subscript) => write!(f, "[{}]", subscript), + AccessExpr::Dot(expr) => write!(f, ".{expr}"), + AccessExpr::Subscript(subscript) => write!(f, "[{subscript}]"), } } } @@ -1361,12 +1433,12 @@ impl fmt::Display for Expr { match self { Expr::Identifier(s) => write!(f, "{s}"), Expr::Wildcard(_) => f.write_str("*"), - Expr::QualifiedWildcard(prefix, _) => write!(f, "{}.*", prefix), + Expr::QualifiedWildcard(prefix, _) => write!(f, "{prefix}.*"), Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")), Expr::CompoundFieldAccess { root, access_chain } => { - write!(f, "{}", root)?; + write!(f, "{root}")?; for field in access_chain { - write!(f, "{}", field)?; + write!(f, "{field}")?; } Ok(()) } @@ -1434,7 +1506,7 @@ impl fmt::Display for Expr { } => match escape_char { Some(ch) => write!( f, - "{} {}LIKE {}{} ESCAPE '{}'", + "{} {}LIKE {}{} ESCAPE {}", expr, if *negated { "NOT " } else { "" }, if *any { "ANY " } else { "" }, @@ -1459,7 +1531,7 @@ impl fmt::Display for Expr { } => match escape_char { Some(ch) => write!( f, - "{} {}ILIKE {}{} ESCAPE '{}'", + "{} {}ILIKE {}{} ESCAPE {}", expr, if *negated { "NOT " } else { "" }, if *any { "ANY" } else { "" }, @@ -1495,7 +1567,7 @@ impl fmt::Display for Expr { } => { let not_ = if *negated { "NOT " } else { "" }; if form.is_none() { - write!(f, "{} IS {}NORMALIZED", expr, not_) + write!(f, "{expr} IS {not_}NORMALIZED") } else { write!( f, @@ -1514,7 +1586,7 @@ impl fmt::Display for Expr { } => match escape_char { Some(ch) => write!( f, - "{} {}SIMILAR TO {} ESCAPE '{}'", + "{} {}SIMILAR TO {} ESCAPE {}", expr, if *negated { "NOT " } else { "" }, pattern, @@ -1658,27 +1730,32 @@ impl fmt::Display for Expr { Expr::Nested(ast) => write!(f, "({ast})"), Expr::Value(v) => write!(f, "{v}"), Expr::Prefixed { prefix, value } => write!(f, "{prefix} {value}"), - Expr::TypedString { data_type, value } => { - write!(f, "{data_type}")?; - write!(f, " {value}") - } - Expr::Function(fun) => write!(f, "{fun}"), + Expr::TypedString(ts) => ts.fmt(f), + Expr::Function(fun) => fun.fmt(f), Expr::Case { + case_token: _, + end_token: _, operand, conditions, else_result, } => { - write!(f, "CASE")?; + f.write_str("CASE")?; if let Some(operand) = operand { - write!(f, " {operand}")?; + f.write_str(" ")?; + operand.fmt(f)?; } for when in conditions { - write!(f, " {when}")?; + SpaceOrNewline.fmt(f)?; + Indent(when).fmt(f)?; } if let Some(else_result) = else_result { - write!(f, " ELSE {else_result}")?; + SpaceOrNewline.fmt(f)?; + Indent("ELSE").fmt(f)?; + SpaceOrNewline.fmt(f)?; + Indent(Indent(else_result)).fmt(f)?; } - write!(f, " END") + SpaceOrNewline.fmt(f)?; + f.write_str("END") } Expr::Exists { subquery, negated } => write!( f, @@ -1809,7 +1886,7 @@ impl fmt::Display for Expr { } } Expr::Named { expr, name } => { - write!(f, "{} AS {}", expr, name) + write!(f, "{expr} AS {name}") } Expr::Dictionary(fields) => { write!(f, "{{{}}}", display_comma_separated(fields)) @@ -1852,6 +1929,7 @@ impl fmt::Display for Expr { } Expr::Prior(expr) => write!(f, "PRIOR {expr}"), Expr::Lambda(lambda) => write!(f, "{lambda}"), + Expr::MemberOf(member_of) => write!(f, "{member_of}"), } } } @@ -1867,8 +1945,14 @@ pub enum WindowType { impl Display for WindowType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - WindowType::WindowSpec(spec) => write!(f, "({})", spec), - WindowType::NamedWindow(name) => write!(f, "{}", name), + WindowType::WindowSpec(spec) => { + f.write_str("(")?; + NewLine.fmt(f)?; + Indent(spec).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")") + } + WindowType::NamedWindow(name) => name.fmt(f), } } } @@ -1896,14 +1980,19 @@ pub struct WindowSpec { impl fmt::Display for WindowSpec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut delim = ""; + let mut is_first = true; if let Some(window_name) = &self.window_name { - delim = " "; + if !is_first { + SpaceOrNewline.fmt(f)?; + } + is_first = false; write!(f, "{window_name}")?; } if !self.partition_by.is_empty() { - f.write_str(delim)?; - delim = " "; + if !is_first { + SpaceOrNewline.fmt(f)?; + } + is_first = false; write!( f, "PARTITION BY {}", @@ -1911,12 +2000,16 @@ impl fmt::Display for WindowSpec { )?; } if !self.order_by.is_empty() { - f.write_str(delim)?; - delim = " "; + if !is_first { + SpaceOrNewline.fmt(f)?; + } + is_first = false; write!(f, "ORDER BY {}", display_comma_separated(&self.order_by))?; } if let Some(window_frame) = &self.window_frame { - f.write_str(delim)?; + if !is_first { + SpaceOrNewline.fmt(f)?; + } if let Some(end_bound) = &window_frame.end_bound { write!( f, @@ -2228,7 +2321,33 @@ impl fmt::Display for IfStatement { } } -/// A block within a [Statement::Case] or [Statement::If]-like statement +/// A `WHILE` statement. +/// +/// Example: +/// ```sql +/// WHILE @@FETCH_STATUS = 0 +/// BEGIN +/// FETCH NEXT FROM c1 INTO @var1, @var2; +/// END +/// ``` +/// +/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/while-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct WhileStatement { + pub while_block: ConditionalStatementBlock, +} + +impl fmt::Display for WhileStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let WhileStatement { while_block } = self; + write!(f, "{while_block}")?; + Ok(()) + } +} + +/// A block within a [Statement::Case] or [Statement::If] or [Statement::While]-like statement /// /// Example 1: /// ```sql @@ -2244,6 +2363,14 @@ impl fmt::Display for IfStatement { /// ```sql /// ELSE SELECT 1; SELECT 2; /// ``` +/// +/// Example 4: +/// ```sql +/// WHILE @@FETCH_STATUS = 0 +/// BEGIN +/// FETCH NEXT FROM c1 INTO @var1, @var2; +/// END +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2316,7 +2443,7 @@ impl fmt::Display for ConditionalStatements { } Ok(()) } - ConditionalStatements::BeginEnd(bes) => write!(f, "{}", bes), + ConditionalStatements::BeginEnd(bes) => write!(f, "{bes}"), } } } @@ -2346,11 +2473,16 @@ impl fmt::Display for BeginEndStatements { end_token: AttachedToken(end_token), } = self; - write!(f, "{begin_token} ")?; + if begin_token.token != Token::EOF { + write!(f, "{begin_token} ")?; + } if !statements.is_empty() { format_statement_list(f, statements)?; } - write!(f, " {end_token}") + if end_token.token != Token::EOF { + write!(f, " {end_token}")?; + } + Ok(()) } } @@ -2647,6 +2779,18 @@ pub enum CreateTableOptions { /// /// Options(Vec), + + /// Plain options, options which are not part on any declerative statement e.g. WITH/OPTIONS/... + /// + Plain(Vec), + + TableProperties(Vec), +} + +impl Default for CreateTableOptions { + fn default() -> Self { + Self::None + } } impl fmt::Display for CreateTableOptions { @@ -2658,6 +2802,12 @@ impl fmt::Display for CreateTableOptions { CreateTableOptions::Options(options) => { write!(f, "OPTIONS({})", display_comma_separated(options)) } + CreateTableOptions::TableProperties(options) => { + write!(f, "TBLPROPERTIES ({})", display_comma_separated(options)) + } + CreateTableOptions::Plain(options) => { + write!(f, "{}", display_separated(options, " ")) + } CreateTableOptions::None => Ok(()), } } @@ -2813,9 +2963,7 @@ impl Display for Set { write!( f, "SET {modifier}ROLE {role_name}", - modifier = context_modifier - .map(|m| format!("{}", m)) - .unwrap_or_default() + modifier = context_modifier.map(|m| format!("{m}")).unwrap_or_default() ) } Self::SetSessionParam(kind) => write!(f, "SET {kind}"), @@ -2848,7 +2996,7 @@ impl Display for Set { charset_name, collation_name, } => { - write!(f, "SET NAMES {}", charset_name)?; + write!(f, "SET NAMES {charset_name}")?; if let Some(collation) = collation_name { f.write_str(" COLLATE ")?; @@ -2871,7 +3019,7 @@ impl Display for Set { write!( f, "SET {}{}{} = {}", - scope.map(|s| format!("{}", s)).unwrap_or_default(), + scope.map(|s| format!("{s}")).unwrap_or_default(), if *hivevar { "HIVEVAR:" } else { "" }, variable, display_comma_separated(values) @@ -2889,6 +3037,36 @@ impl From for Statement { } } +/// A representation of a `WHEN` arm with all the identifiers catched and the statements to execute +/// for the arm. +/// +/// Snowflake: +/// BigQuery: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ExceptionWhen { + pub idents: Vec, + pub statements: Vec, +} + +impl Display for ExceptionWhen { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "WHEN {idents} THEN", + idents = display_separated(&self.idents, " OR ") + )?; + + if !self.statements.is_empty() { + write!(f, " ")?; + format_statement_list(f, &self.statements)?; + } + + Ok(()) + } +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -2925,9 +3103,6 @@ pub enum Statement { /// TABLE - optional keyword; table: bool, /// Postgres-specific option - /// [ TRUNCATE TABLE ONLY ] - only: bool, - /// Postgres-specific option /// [ RESTART IDENTITY | CONTINUE IDENTITY ] identity: Option, /// Postgres-specific option @@ -2983,6 +3158,8 @@ pub enum Statement { Case(CaseStatement), /// An `IF` statement. If(IfStatement), + /// A `WHILE` statement. + While(WhileStatement), /// A `RAISE` statement. Raise(RaiseStatement), /// ```sql @@ -3034,6 +3211,11 @@ pub enum Statement { partition: Option>, }, /// ```sql + /// OPEN cursor_name + /// ``` + /// Opens a cursor. + Open(OpenStatement), + /// ```sql /// CLOSE /// ``` /// Closes the portal underlying an open cursor. @@ -3057,6 +3239,8 @@ pub enum Statement { returning: Option>, /// SQLite-specific conflict resolution clause or: Option, + /// LIMIT + limit: Option, }, /// ```sql /// DELETE @@ -3074,6 +3258,17 @@ pub enum Statement { materialized: bool, /// View name name: ObjectName, + /// If `if_not_exists` is true, this flag is set to true if the view name comes before the `IF NOT EXISTS` clause. + /// Example: + /// ```sql + /// CREATE VIEW myview IF NOT EXISTS AS SELECT 1` + /// ``` + /// Otherwise, the flag is set to false if the view name comes after the clause + /// Example: + /// ```sql + /// CREATE VIEW IF NOT EXISTS myview AS SELECT 1` + /// ``` + name_before_not_exists: bool, columns: Vec, query: Box, options: CreateTableOptions, @@ -3151,6 +3346,8 @@ pub enum Statement { secret_type: Ident, options: Vec, }, + /// A `CREATE SERVER` statement. + CreateServer(CreateServerStatement), /// ```sql /// CREATE POLICY /// ``` @@ -3185,8 +3382,18 @@ pub enum Statement { /// For example: `ALTER TABLE table_name ON CLUSTER cluster_name ADD COLUMN c UInt32` /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/update) on_cluster: Option, + /// Snowflake "ICEBERG" clause for Iceberg tables + /// + iceberg: bool, + /// Token that represents the end of the statement (semicolon or EOF) + end_token: AttachedToken, }, /// ```sql + /// ALTER SCHEMA + /// ``` + /// See [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#alter_schema_collate_statement) + AlterSchema(AlterSchema), + /// ```sql /// ALTER INDEX /// ``` AlterIndex { @@ -3309,6 +3516,9 @@ pub enum Statement { purge: bool, /// MySQL-specific "TEMPORARY" keyword temporary: bool, + /// MySQL-specific drop index syntax, which requires table specification + /// See + table: Option, }, /// ```sql /// DROP FUNCTION @@ -3321,6 +3531,14 @@ pub enum Statement { drop_behavior: Option, }, /// ```sql + /// DROP DOMAIN + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-dropdomain.html) + /// + /// DROP DOMAIN [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + /// + DropDomain(DropDomain), + /// ```sql /// DROP PROCEDURE /// ``` DropProcedure { @@ -3405,6 +3623,7 @@ pub enum Statement { /// Cursor name name: Ident, direction: FetchDirection, + position: FetchPosition, /// Optional, It's possible to fetch rows form cursor to the table into: Option, }, @@ -3498,6 +3717,12 @@ pub enum Statement { history: bool, show_options: ShowStatementOptions, }, + // ```sql + // SHOW {CHARACTER SET | CHARSET} + // ``` + // [MySQL]: + // + ShowCharset(ShowCharset), /// ```sql /// SHOW OBJECTS LIKE 'line%' IN mydb.public /// ``` @@ -3558,17 +3783,20 @@ pub enum Statement { /// END; /// ``` statements: Vec, - /// Statements of an exception clause. + /// Exception handling with exception clauses. /// Example: /// ```sql - /// BEGIN - /// SELECT 1; - /// EXCEPTION WHEN ERROR THEN - /// SELECT 2; - /// SELECT 3; - /// END; + /// EXCEPTION + /// WHEN EXCEPTION_1 THEN + /// SELECT 2; + /// WHEN EXCEPTION_2 OR EXCEPTION_3 THEN + /// SELECT 3; + /// WHEN OTHER THEN + /// SELECT 4; + /// ``` /// - exception_statements: Option>, + /// + exception: Option>, /// TRUE if the statement has an `END` keyword. has_end_keyword: bool, }, @@ -3613,6 +3841,14 @@ pub enum Statement { /// ` | AUTHORIZATION | AUTHORIZATION ` schema_name: SchemaName, if_not_exists: bool, + /// Schema properties. + /// + /// ```sql + /// CREATE SCHEMA myschema WITH (key1='value1'); + /// ``` + /// + /// [Trino](https://trino.io/docs/current/sql/create-schema.html) + with: Option>, /// Schema options. /// /// ```sql @@ -3629,15 +3865,41 @@ pub enum Statement { /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_schema_statement) default_collate_spec: Option, + /// Clones a schema + /// + /// ```sql + /// CREATE SCHEMA myschema CLONE otherschema + /// ``` + /// + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-clone#databases-schemas) + clone: Option, }, /// ```sql /// CREATE DATABASE /// ``` + /// See: + /// CreateDatabase { db_name: ObjectName, if_not_exists: bool, location: Option, managed_location: Option, + or_replace: bool, + transient: bool, + clone: Option, + data_retention_time_in_days: Option, + max_data_extension_time_in_days: Option, + external_volume: Option, + catalog: Option, + replace_invalid_characters: Option, + default_ddl_collation: Option, + storage_serialization_policy: Option, + comment: Option, + catalog_sync: Option, + catalog_sync_namespace_mode: Option, + catalog_sync_namespace_flatten_delimiter: Option, + with_tags: Option>, + with_contacts: Option>, }, /// ```sql /// CREATE FUNCTION @@ -3661,7 +3923,12 @@ pub enum Statement { /// ``` /// /// Postgres: + /// SQL Server: CreateTrigger { + /// True if this is a `CREATE OR ALTER TRIGGER` statement + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver16#arguments) + or_alter: bool, /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. /// /// Example: @@ -3705,6 +3972,15 @@ pub enum Statement { /// EXECUTE FUNCTION trigger_function(); /// ``` period: TriggerPeriod, + /// Whether the trigger period was specified before the target table name. + /// + /// ```sql + /// -- period_before_table == true: Postgres, MySQL, and standard SQL + /// CREATE TRIGGER t BEFORE INSERT ON table_name ...; + /// -- period_before_table == false: MSSQL + /// CREATE TRIGGER t ON table_name BEFORE INSERT ...; + /// ``` + period_before_table: bool, /// Multiple events can be specified using OR, such as `INSERT`, `UPDATE`, `DELETE`, or `TRUNCATE`. events: Vec, /// The table on which the trigger is to be created. @@ -3722,7 +3998,11 @@ pub enum Statement { /// Triggering conditions condition: Option, /// Execute logic block - exec_body: TriggerExecBody, + exec_body: Option, + /// For MSSQL and dialects where statements are preceded by `AS` + statements_as: bool, + /// For SQL dialects with statement(s) for a body + statements: Option, /// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`, characteristics: Option, }, @@ -3746,7 +4026,8 @@ pub enum Statement { or_alter: bool, name: ObjectName, params: Option>, - body: Vec, + language: Option, + body: ConditionalStatements, }, /// ```sql /// CREATE MACRO @@ -3791,9 +4072,15 @@ pub enum Statement { objects: Option, grantees: Vec, with_grant_option: bool, + as_grantor: Option, granted_by: Option, + current_grants: Option, }, /// ```sql + /// DENY privileges ON object TO grantees + /// ``` + Deny(DenyStatement), + /// ```sql /// REVOKE privileges ON objects FROM grantees /// ``` Revoke { @@ -3828,6 +4115,12 @@ pub enum Statement { immediate: bool, into: Vec, using: Vec, + /// Whether the last parameter is the return value of the procedure + /// MSSQL: + output: bool, + /// Whether to invoke the procedure with the default parameter values + /// MSSQL: + default: bool, }, /// ```sql /// PREPARE name [ ( data_type [, ...] ) ] AS statement @@ -3889,7 +4182,7 @@ pub enum Statement { /// A SQL query that specifies what to explain statement: Box, /// Optional output format of explain - format: Option, + format: Option, /// Postgres style utility options, `(analyze, verbose true)` options: Option>, }, @@ -3969,6 +4262,8 @@ pub enum Statement { sequence_options: Vec, owned_by: Option, }, + /// A `CREATE DOMAIN` statement. + CreateDomain(CreateDomain), /// ```sql /// CREATE TYPE /// ``` @@ -4039,7 +4334,7 @@ pub enum Statement { /// ```sql /// NOTIFY channel [ , payload ] /// ``` - /// send a notification event together with an optional “payload” string to channel + /// send a notification event together with an optional "payload" string to channel /// /// See Postgres NOTIFY { @@ -4100,6 +4395,49 @@ pub enum Statement { /// /// See [ReturnStatement] Return(ReturnStatement), + /// Export data statement + /// + /// Example: + /// ```sql + /// EXPORT DATA OPTIONS(uri='gs://bucket/folder/*', format='PARQUET', overwrite=true) AS + /// SELECT field1, field2 FROM mydataset.table1 ORDER BY field1 LIMIT 10 + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/export-statements) + ExportData(ExportData), + /// ```sql + /// CREATE [OR REPLACE] USER [IF NOT EXISTS] + /// ``` + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-user) + CreateUser(CreateUser), + /// Re-sorts rows and reclaims space in either a specified table or all tables in the current database + /// + /// ```sql + /// VACUUM tbl + /// ``` + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html) + Vacuum(VacuumStatement), +} + +/// ```sql +/// {COPY | REVOKE} CURRENT GRANTS +/// ``` +/// +/// - [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#optional-parameters) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CurrentGrantsKind { + CopyCurrentGrants, + RevokeCurrentGrants, +} + +impl fmt::Display for CurrentGrantsKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CurrentGrantsKind::CopyCurrentGrants => write!(f, "COPY CURRENT GRANTS"), + CurrentGrantsKind::RevokeCurrentGrants => write!(f, "REVOKE CURRENT GRANTS"), + } + } } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -4122,6 +4460,28 @@ impl fmt::Display for RaisErrorOption { } impl fmt::Display for Statement { + /// Formats a SQL statement with support for pretty printing. + /// + /// When using the alternate flag (`{:#}`), the statement will be formatted with proper + /// indentation and line breaks. For example: + /// + /// ``` + /// # use sqlparser::dialect::GenericDialect; + /// # use sqlparser::parser::Parser; + /// let sql = "SELECT a, b FROM table_1"; + /// let ast = Parser::parse_sql(&GenericDialect, sql).unwrap(); + /// + /// // Regular formatting + /// assert_eq!(format!("{}", ast[0]), "SELECT a, b FROM table_1"); + /// + /// // Pretty printing + /// assert_eq!(format!("{:#}", ast[0]), + /// r#"SELECT + /// a, + /// b + /// FROM + /// table_1"#); + /// ``` // Clippy thinks this function is too complicated, but it is painful to // split up without extracting structs for each `Statement` variant. #[allow(clippy::cognitive_complexity)] @@ -4137,7 +4497,8 @@ impl fmt::Display for Statement { } => { write!(f, "FLUSH")?; if let Some(location) = location { - write!(f, " {location}")?; + f.write_str(" ")?; + location.fmt(f)?; } write!(f, " {object_type}")?; @@ -4175,7 +4536,7 @@ impl fmt::Display for Statement { write!(f, "{describe_alias} ")?; if let Some(format) = hive_format { - write!(f, "{} ", format)?; + write!(f, "{format} ")?; } if *has_table_keyword { write!(f, "TABLE ")?; @@ -4210,7 +4571,7 @@ impl fmt::Display for Statement { } if let Some(format) = format { - write!(f, "FORMAT {format} ")?; + write!(f, "{format} ")?; } if let Some(options) = options { @@ -4219,7 +4580,7 @@ impl fmt::Display for Statement { write!(f, "{statement}") } - Statement::Query(s) => write!(f, "{s}"), + Statement::Query(s) => s.fmt(f), Statement::Declare { stmts } => { write!(f, "DECLARE ")?; write!(f, "{}", display_separated(stmts, "; ")) @@ -4227,11 +4588,10 @@ impl fmt::Display for Statement { Statement::Fetch { name, direction, + position, into, } => { - write!(f, "FETCH {direction} ")?; - - write!(f, "IN {name}")?; + write!(f, "FETCH {direction} {position} {name}")?; if let Some(into) = into { write!(f, " INTO {into}")?; @@ -4278,17 +4638,15 @@ impl fmt::Display for Statement { table_names, partitions, table, - only, identity, cascade, on_cluster, } => { let table = if *table { "TABLE " } else { "" }; - let only = if *only { "ONLY " } else { "" }; write!( f, - "TRUNCATE {table}{only}{table_names}", + "TRUNCATE {table}{table_names}", table_names = display_comma_separated(table_names) )?; @@ -4321,6 +4679,9 @@ impl fmt::Display for Statement { Statement::If(stmt) => { write!(f, "{stmt}") } + Statement::While(stmt) => { + write!(f, "{stmt}") + } Statement::Raise(stmt) => { write!(f, "{stmt}") } @@ -4404,7 +4765,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Insert(insert) => write!(f, "{insert}"), + Statement::Insert(insert) => insert.fmt(f), Statement::Install { extension_name: name, } => write!(f, "INSTALL {name}"), @@ -4466,30 +4827,48 @@ impl fmt::Display for Statement { selection, returning, or, + limit, } => { - write!(f, "UPDATE ")?; + f.write_str("UPDATE ")?; if let Some(or) = or { - write!(f, "{or} ")?; + or.fmt(f)?; + f.write_str(" ")?; } - write!(f, "{table}")?; + table.fmt(f)?; if let Some(UpdateTableFromKind::BeforeSet(from)) = from { - write!(f, " FROM {}", display_comma_separated(from))?; + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; } if !assignments.is_empty() { - write!(f, " SET {}", display_comma_separated(assignments))?; + SpaceOrNewline.fmt(f)?; + f.write_str("SET")?; + indented_list(f, assignments)?; } if let Some(UpdateTableFromKind::AfterSet(from)) = from { - write!(f, " FROM {}", display_comma_separated(from))?; + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; } if let Some(selection) = selection { - write!(f, " WHERE {selection}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("WHERE")?; + SpaceOrNewline.fmt(f)?; + Indent(selection).fmt(f)?; } if let Some(returning) = returning { - write!(f, " RETURNING {}", display_comma_separated(returning))?; + SpaceOrNewline.fmt(f)?; + f.write_str("RETURNING")?; + indented_list(f, returning)?; + } + if let Some(limit) = limit { + SpaceOrNewline.fmt(f)?; + write!(f, "LIMIT {limit}")?; } Ok(()) } - Statement::Delete(delete) => write!(f, "{delete}"), + Statement::Delete(delete) => delete.fmt(f), + Statement::Open(open) => open.fmt(f), Statement::Close { cursor } => { write!(f, "CLOSE {cursor}")?; @@ -4500,25 +4879,105 @@ impl fmt::Display for Statement { if_not_exists, location, managed_location, + or_replace, + transient, + clone, + data_retention_time_in_days, + max_data_extension_time_in_days, + external_volume, + catalog, + replace_invalid_characters, + default_ddl_collation, + storage_serialization_policy, + comment, + catalog_sync, + catalog_sync_namespace_mode, + catalog_sync_namespace_flatten_delimiter, + with_tags, + with_contacts, } => { - write!(f, "CREATE DATABASE")?; - if *if_not_exists { - write!(f, " IF NOT EXISTS")?; - } - write!(f, " {db_name}")?; + write!( + f, + "CREATE {or_replace}{transient}DATABASE {if_not_exists}{name}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + transient = if *transient { "TRANSIENT " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + name = db_name, + )?; + if let Some(l) = location { write!(f, " LOCATION '{l}'")?; } if let Some(ml) = managed_location { write!(f, " MANAGEDLOCATION '{ml}'")?; } + if let Some(clone) = clone { + write!(f, " CLONE {clone}")?; + } + + if let Some(value) = data_retention_time_in_days { + write!(f, " DATA_RETENTION_TIME_IN_DAYS = {value}")?; + } + + if let Some(value) = max_data_extension_time_in_days { + write!(f, " MAX_DATA_EXTENSION_TIME_IN_DAYS = {value}")?; + } + + if let Some(vol) = external_volume { + write!(f, " EXTERNAL_VOLUME = '{vol}'")?; + } + + if let Some(cat) = catalog { + write!(f, " CATALOG = '{cat}'")?; + } + + if let Some(true) = replace_invalid_characters { + write!(f, " REPLACE_INVALID_CHARACTERS = TRUE")?; + } else if let Some(false) = replace_invalid_characters { + write!(f, " REPLACE_INVALID_CHARACTERS = FALSE")?; + } + + if let Some(collation) = default_ddl_collation { + write!(f, " DEFAULT_DDL_COLLATION = '{collation}'")?; + } + + if let Some(policy) = storage_serialization_policy { + write!(f, " STORAGE_SERIALIZATION_POLICY = {policy}")?; + } + + if let Some(comment) = comment { + write!(f, " COMMENT = '{comment}'")?; + } + + if let Some(sync) = catalog_sync { + write!(f, " CATALOG_SYNC = '{sync}'")?; + } + + if let Some(mode) = catalog_sync_namespace_mode { + write!(f, " CATALOG_SYNC_NAMESPACE_MODE = {mode}")?; + } + + if let Some(delim) = catalog_sync_namespace_flatten_delimiter { + write!(f, " CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '{delim}'")?; + } + + if let Some(tags) = with_tags { + write!(f, " WITH TAG ({})", display_comma_separated(tags))?; + } + + if let Some(contacts) = with_contacts { + write!(f, " WITH CONTACT ({})", display_comma_separated(contacts))?; + } Ok(()) } Statement::CreateFunction(create_function) => create_function.fmt(f), + Statement::CreateDomain(create_domain) => create_domain.fmt(f), Statement::CreateTrigger { + or_alter, or_replace, is_constraint, name, + period_before_table, period, events, table_name, @@ -4528,19 +4987,31 @@ impl fmt::Display for Statement { condition, include_each, exec_body, + statements_as, + statements, characteristics, } => { write!( f, - "CREATE {or_replace}{is_constraint}TRIGGER {name} {period}", + "CREATE {or_alter}{or_replace}{is_constraint}TRIGGER {name} ", + or_alter = if *or_alter { "OR ALTER " } else { "" }, or_replace = if *or_replace { "OR REPLACE " } else { "" }, is_constraint = if *is_constraint { "CONSTRAINT " } else { "" }, )?; - if !events.is_empty() { - write!(f, " {}", display_separated(events, " OR "))?; + if *period_before_table { + write!(f, "{period}")?; + if !events.is_empty() { + write!(f, " {}", display_separated(events, " OR "))?; + } + write!(f, " ON {table_name}")?; + } else { + write!(f, "ON {table_name}")?; + write!(f, " {period}")?; + if !events.is_empty() { + write!(f, " {}", display_separated(events, ", "))?; + } } - write!(f, " ON {table_name}")?; if let Some(referenced_table_name) = referenced_table_name { write!(f, " FROM {referenced_table_name}")?; @@ -4556,13 +5027,22 @@ impl fmt::Display for Statement { if *include_each { write!(f, " FOR EACH {trigger_object}")?; - } else { + } else if exec_body.is_some() { write!(f, " FOR {trigger_object}")?; } if let Some(condition) = condition { write!(f, " WHEN {condition}")?; } - write!(f, " EXECUTE {exec_body}") + if let Some(exec_body) = exec_body { + write!(f, " EXECUTE {exec_body}")?; + } + if let Some(statements) = statements { + if *statements_as { + write!(f, " AS")?; + } + write!(f, " {statements}")?; + } + Ok(()) } Statement::DropTrigger { if_exists, @@ -4587,6 +5067,7 @@ impl fmt::Display for Statement { name, or_alter, params, + language, body, } => { write!( @@ -4601,11 +5082,12 @@ impl fmt::Display for Statement { write!(f, " ({})", display_comma_separated(p))?; } } - write!( - f, - " AS BEGIN {body} END", - body = display_separated(body, "; ") - ) + + if let Some(language) = language { + write!(f, " LANGUAGE {language}")?; + } + + write!(f, " AS {body}") } Statement::CreateMacro { or_replace, @@ -4644,6 +5126,7 @@ impl fmt::Display for Statement { temporary, to, params, + name_before_not_exists, } => { write!( f, @@ -4656,11 +5139,18 @@ impl fmt::Display for Statement { } write!( f, - "{materialized}{temporary}VIEW {if_not_exists}{name}{to}", + "{materialized}{temporary}VIEW {if_not_and_name}{to}", + if_not_and_name = if *if_not_exists { + if *name_before_not_exists { + format!("{name} IF NOT EXISTS") + } else { + format!("IF NOT EXISTS {name}") + } + } else { + format!("{name}") + }, materialized = if *materialized { "MATERIALIZED " } else { "" }, - name = name, temporary = if *temporary { "TEMPORARY " } else { "" }, - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, to = to .as_ref() .map(|to| format!(" TO {to}")) @@ -4685,7 +5175,9 @@ impl fmt::Display for Statement { if matches!(options, CreateTableOptions::Options(_)) { write!(f, " {options}")?; } - write!(f, " AS {query}")?; + f.write_str(" AS")?; + SpaceOrNewline.fmt(f)?; + query.fmt(f)?; if *with_no_schema_binding { write!(f, " WITH NO SCHEMA BINDING")?; } @@ -4911,6 +5403,9 @@ impl fmt::Display for Statement { write!(f, " )")?; Ok(()) } + Statement::CreateServer(stmt) => { + write!(f, "{stmt}") + } Statement::CreatePolicy { name, table_name, @@ -4961,15 +5456,22 @@ impl fmt::Display for Statement { operations, location, on_cluster, + iceberg, + end_token: _, } => { - write!(f, "ALTER TABLE ")?; + if *iceberg { + write!(f, "ALTER ICEBERG TABLE ")?; + } else { + write!(f, "ALTER TABLE ")?; + } + if *if_exists { write!(f, "IF EXISTS ")?; } if *only { write!(f, "ONLY ")?; } - write!(f, "{name} ", name = name)?; + write!(f, "{name} ")?; if let Some(cluster) = on_cluster { write!(f, "ON CLUSTER {cluster} ")?; } @@ -5047,7 +5549,7 @@ impl fmt::Display for Statement { )?; if !session_params.options.is_empty() { if *set { - write!(f, " {}", session_params)?; + write!(f, " {session_params}")?; } else { let options = session_params .options @@ -5067,17 +5569,24 @@ impl fmt::Display for Statement { restrict, purge, temporary, - } => write!( - f, - "DROP {}{}{} {}{}{}{}", - if *temporary { "TEMPORARY " } else { "" }, - object_type, - if *if_exists { " IF EXISTS" } else { "" }, - display_comma_separated(names), - if *cascade { " CASCADE" } else { "" }, - if *restrict { " RESTRICT" } else { "" }, - if *purge { " PURGE" } else { "" } - ), + table, + } => { + write!( + f, + "DROP {}{}{} {}{}{}{}", + if *temporary { "TEMPORARY " } else { "" }, + object_type, + if *if_exists { " IF EXISTS" } else { "" }, + display_comma_separated(names), + if *cascade { " CASCADE" } else { "" }, + if *restrict { " RESTRICT" } else { "" }, + if *purge { " PURGE" } else { "" }, + )?; + if let Some(table_name) = table.as_ref() { + write!(f, " ON {table_name}")?; + }; + Ok(()) + } Statement::DropFunction { if_exists, func_desc, @@ -5094,6 +5603,21 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::DropDomain(DropDomain { + if_exists, + name, + drop_behavior, + }) => { + write!( + f, + "DROP DOMAIN{} {name}", + if *if_exists { " IF EXISTS" } else { "" }, + )?; + if let Some(op) = drop_behavior { + write!(f, " {op}")?; + } + Ok(()) + } Statement::DropProcedure { if_exists, proc_desc, @@ -5303,18 +5827,19 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::ShowCharset(show_stm) => show_stm.fmt(f), Statement::StartTransaction { modes, begin: syntax_begin, transaction, modifier, statements, - exception_statements, + exception, has_end_keyword, } => { if *syntax_begin { if let Some(modifier) = *modifier { - write!(f, "BEGIN {}", modifier)?; + write!(f, "BEGIN {modifier}")?; } else { write!(f, "BEGIN")?; } @@ -5331,11 +5856,10 @@ impl fmt::Display for Statement { write!(f, " ")?; format_statement_list(f, statements)?; } - if let Some(exception_statements) = exception_statements { - write!(f, " EXCEPTION WHEN ERROR THEN")?; - if !exception_statements.is_empty() { - write!(f, " ")?; - format_statement_list(f, exception_statements)?; + if let Some(exception_when) = exception { + write!(f, " EXCEPTION")?; + for when in exception_when { + write!(f, " {when}")?; } } if *has_end_keyword { @@ -5351,7 +5875,7 @@ impl fmt::Display for Statement { if *end_syntax { write!(f, "END")?; if let Some(modifier) = *modifier { - write!(f, " {}", modifier)?; + write!(f, " {modifier}")?; } if *chain { write!(f, " AND CHAIN")?; @@ -5377,8 +5901,10 @@ impl fmt::Display for Statement { Statement::CreateSchema { schema_name, if_not_exists, + with, options, default_collate_spec, + clone, } => { write!( f, @@ -5391,10 +5917,17 @@ impl fmt::Display for Statement { write!(f, " DEFAULT COLLATE {collate}")?; } + if let Some(with) = with { + write!(f, " WITH ({})", display_comma_separated(with))?; + } + if let Some(options) = options { write!(f, " OPTIONS({})", display_comma_separated(options))?; } + if let Some(clone) = clone { + write!(f, " CLONE {clone}")?; + } Ok(()) } Statement::Assert { condition, message } => { @@ -5409,7 +5942,9 @@ impl fmt::Display for Statement { objects, grantees, with_grant_option, + as_grantor, granted_by, + current_grants, } => { write!(f, "GRANT {privileges} ")?; if let Some(objects) = objects { @@ -5419,11 +5954,18 @@ impl fmt::Display for Statement { if *with_grant_option { write!(f, " WITH GRANT OPTION")?; } + if let Some(current_grants) = current_grants { + write!(f, " {current_grants}")?; + } + if let Some(grantor) = as_grantor { + write!(f, " AS {grantor}")?; + } if let Some(grantor) = granted_by { write!(f, " GRANTED BY {grantor}")?; } Ok(()) } + Statement::Deny(s) => write!(f, "{s}"), Statement::Revoke { privileges, objects, @@ -5440,7 +5982,7 @@ impl fmt::Display for Statement { write!(f, " GRANTED BY {grantor}")?; } if let Some(cascade) = cascade { - write!(f, " {}", cascade)?; + write!(f, " {cascade}")?; } Ok(()) } @@ -5457,6 +5999,8 @@ impl fmt::Display for Statement { immediate, into, using, + output, + default, } => { let (open, close) = if *has_parentheses { ("(", ")") @@ -5477,6 +6021,12 @@ impl fmt::Display for Statement { if !using.is_empty() { write!(f, " USING {}", display_comma_separated(using))?; }; + if *output { + write!(f, " OUTPUT")?; + } + if *default { + write!(f, " DEFAULT")?; + } Ok(()) } Statement::Prepare { @@ -5619,13 +6169,13 @@ impl fmt::Display for Statement { if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, )?; if !directory_table_params.options.is_empty() { - write!(f, " DIRECTORY=({})", directory_table_params)?; + write!(f, " DIRECTORY=({directory_table_params})")?; } if !file_format.options.is_empty() { - write!(f, " FILE_FORMAT=({})", file_format)?; + write!(f, " FILE_FORMAT=({file_format})")?; } if !copy_options.options.is_empty() { - write!(f, " COPY_OPTIONS=({})", copy_options)?; + write!(f, " COPY_OPTIONS=({copy_options})")?; } if comment.is_some() { write!(f, " COMMENT='{}'", comment.as_ref().unwrap())?; @@ -5648,7 +6198,7 @@ impl fmt::Display for Statement { validation_mode, partition, } => { - write!(f, "COPY INTO {}", into)?; + write!(f, "COPY INTO {into}")?; if let Some(into_columns) = into_columns { write!(f, " ({})", display_comma_separated(into_columns))?; } @@ -5664,12 +6214,12 @@ impl fmt::Display for Statement { )?; } if let Some(from_obj_alias) = from_obj_alias { - write!(f, " AS {}", from_obj_alias)?; + write!(f, " AS {from_obj_alias}")?; } write!(f, ")")?; } else if let Some(from_obj) = from_obj { // Standard data load - write!(f, " FROM {}{}", from_obj, stage_params)?; + write!(f, " FROM {from_obj}{stage_params}")?; if let Some(from_obj_alias) = from_obj_alias { write!(f, " AS {from_obj_alias}")?; } @@ -5682,24 +6232,24 @@ impl fmt::Display for Statement { write!(f, " FILES = ('{}')", display_separated(files, "', '"))?; } if let Some(pattern) = pattern { - write!(f, " PATTERN = '{}'", pattern)?; + write!(f, " PATTERN = '{pattern}'")?; } if let Some(partition) = partition { write!(f, " PARTITION BY {partition}")?; } if !file_format.options.is_empty() { - write!(f, " FILE_FORMAT=({})", file_format)?; + write!(f, " FILE_FORMAT=({file_format})")?; } if !copy_options.options.is_empty() { match kind { CopyIntoSnowflakeKind::Table => { - write!(f, " COPY_OPTIONS=({})", copy_options)? + write!(f, " COPY_OPTIONS=({copy_options})")? } CopyIntoSnowflakeKind::Location => write!(f, " {copy_options}")?, } } if let Some(validation_mode) = validation_mode { - write!(f, " VALIDATION_MODE = {}", validation_mode)?; + write!(f, " VALIDATION_MODE = {validation_mode}")?; } Ok(()) } @@ -5745,10 +6295,10 @@ impl fmt::Display for Statement { } => { write!(f, "OPTIMIZE TABLE {name}")?; if let Some(on_cluster) = on_cluster { - write!(f, " ON CLUSTER {on_cluster}", on_cluster = on_cluster)?; + write!(f, " ON CLUSTER {on_cluster}")?; } if let Some(partition) = partition { - write!(f, " {partition}", partition = partition)?; + write!(f, " {partition}")?; } if *include_final { write!(f, " FINAL")?; @@ -5797,6 +6347,10 @@ impl fmt::Display for Statement { Statement::Return(r) => write!(f, "{r}"), Statement::List(command) => write!(f, "LIST {command}"), Statement::Remove(command) => write!(f, "REMOVE {command}"), + Statement::ExportData(e) => write!(f, "{e}"), + Statement::CreateUser(s) => write!(f, "{s}"), + Statement::AlterSchema(s) => write!(f, "{s}"), + Statement::Vacuum(s) => write!(f, "{s}"), } } } @@ -5875,7 +6429,7 @@ impl fmt::Display for SetAssignment { write!( f, "{}{} = {}", - self.scope.map(|s| format!("{}", s)).unwrap_or_default(), + self.scope.map(|s| format!("{s}")).unwrap_or_default(), self.name, self.value ) @@ -5892,10 +6446,17 @@ pub struct TruncateTableTarget { /// name of the table being truncated #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub name: ObjectName, + /// Postgres-specific option + /// [ TRUNCATE TABLE ONLY ] + /// + pub only: bool, } impl fmt::Display for TruncateTableTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.only { + write!(f, "ONLY ")?; + }; write!(f, "{}", self.name) } } @@ -6164,6 +6725,28 @@ impl fmt::Display for FetchDirection { } } +/// The "position" for a FETCH statement. +/// +/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/fetch-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FetchPosition { + From, + In, +} + +impl fmt::Display for FetchPosition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FetchPosition::From => f.write_str("FROM")?, + FetchPosition::In => f.write_str("IN")?, + }; + + Ok(()) + } +} + /// A privilege on a database object (table, sequence, etc.). #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -6186,7 +6769,11 @@ pub enum Action { role: ObjectName, }, Delete, + Drop, EvolveSchema, + Exec { + obj_type: Option, + }, Execute { obj_type: Option, }, @@ -6219,7 +6806,7 @@ pub enum Action { Replicate, ResolveAll, Role { - role: Ident, + role: ObjectName, }, Select { columns: Option>, @@ -6252,7 +6839,14 @@ impl fmt::Display for Action { } Action::DatabaseRole { role } => write!(f, "DATABASE ROLE {role}")?, Action::Delete => f.write_str("DELETE")?, + Action::Drop => f.write_str("DROP")?, Action::EvolveSchema => f.write_str("EVOLVE SCHEMA")?, + Action::Exec { obj_type } => { + f.write_str("EXEC")?; + if let Some(obj_type) = obj_type { + write!(f, " {obj_type}")? + } + } Action::Execute { obj_type } => { f.write_str("EXECUTE")?; if let Some(obj_type) = obj_type { @@ -6329,6 +6923,7 @@ pub enum ActionCreateObjectType { OrganiationListing, ReplicationGroup, Role, + Schema, Share, User, Warehouse, @@ -6350,6 +6945,7 @@ impl fmt::Display for ActionCreateObjectType { ActionCreateObjectType::OrganiationListing => write!(f, "ORGANIZATION LISTING"), ActionCreateObjectType::ReplicationGroup => write!(f, "REPLICATION GROUP"), ActionCreateObjectType::Role => write!(f, "ROLE"), + ActionCreateObjectType::Schema => write!(f, "SCHEMA"), ActionCreateObjectType::Share => write!(f, "SHARE"), ActionCreateObjectType::User => write!(f, "USER"), ActionCreateObjectType::Warehouse => write!(f, "WAREHOUSE"), @@ -6566,7 +7162,7 @@ impl fmt::Display for GranteeName { match self { GranteeName::ObjectName(name) => name.fmt(f), GranteeName::UserHost { user, host } => { - write!(f, "{}@{}", user, host) + write!(f, "{user}@{host}") } } } @@ -6581,6 +7177,26 @@ pub enum GrantObjects { AllSequencesInSchema { schemas: Vec }, /// Grant privileges on `ALL TABLES IN SCHEMA [, ...]` AllTablesInSchema { schemas: Vec }, + /// Grant privileges on `ALL VIEWS IN SCHEMA [, ...]` + AllViewsInSchema { schemas: Vec }, + /// Grant privileges on `ALL MATERIALIZED VIEWS IN SCHEMA [, ...]` + AllMaterializedViewsInSchema { schemas: Vec }, + /// Grant privileges on `ALL EXTERNAL TABLES IN SCHEMA [, ...]` + AllExternalTablesInSchema { schemas: Vec }, + /// Grant privileges on `ALL FUNCTIONS IN SCHEMA [, ...]` + AllFunctionsInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE SCHEMAS IN DATABASE [, ...]` + FutureSchemasInDatabase { databases: Vec }, + /// Grant privileges on `FUTURE TABLES IN SCHEMA [, ...]` + FutureTablesInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE VIEWS IN SCHEMA [, ...]` + FutureViewsInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE EXTERNAL TABLES IN SCHEMA [, ...]` + FutureExternalTablesInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE MATERIALIZED VIEWS IN SCHEMA [, ...]` + FutureMaterializedViewsInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE SEQUENCES IN SCHEMA [, ...]` + FutureSequencesInSchema { schemas: Vec }, /// Grant privileges on specific databases Databases(Vec), /// Grant privileges on specific schemas @@ -6609,6 +7225,25 @@ pub enum GrantObjects { ReplicationGroup(Vec), /// Grant privileges on external volumes ExternalVolumes(Vec), + /// Grant privileges on a procedure. In dialects that + /// support overloading, the argument types must be specified. + /// + /// For example: + /// `GRANT USAGE ON PROCEDURE foo(varchar) TO ROLE role1` + Procedure { + name: ObjectName, + arg_types: Vec, + }, + + /// Grant privileges on a function. In dialects that + /// support overloading, the argument types must be specified. + /// + /// For example: + /// `GRANT USAGE ON FUNCTION foo(varchar) TO ROLE role1` + Function { + name: ObjectName, + arg_types: Vec, + }, } impl fmt::Display for GrantObjects { @@ -6649,6 +7284,76 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::AllExternalTablesInSchema { schemas } => { + write!( + f, + "ALL EXTERNAL TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllViewsInSchema { schemas } => { + write!( + f, + "ALL VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllMaterializedViewsInSchema { schemas } => { + write!( + f, + "ALL MATERIALIZED VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllFunctionsInSchema { schemas } => { + write!( + f, + "ALL FUNCTIONS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureSchemasInDatabase { databases } => { + write!( + f, + "FUTURE SCHEMAS IN DATABASE {}", + display_comma_separated(databases) + ) + } + GrantObjects::FutureTablesInSchema { schemas } => { + write!( + f, + "FUTURE TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureExternalTablesInSchema { schemas } => { + write!( + f, + "FUTURE EXTERNAL TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureViewsInSchema { schemas } => { + write!( + f, + "FUTURE VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureMaterializedViewsInSchema { schemas } => { + write!( + f, + "FUTURE MATERIALIZED VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureSequencesInSchema { schemas } => { + write!( + f, + "FUTURE SEQUENCES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::ResourceMonitors(objects) => { write!(f, "RESOURCE MONITOR {}", display_comma_separated(objects)) } @@ -6670,7 +7375,52 @@ impl fmt::Display for GrantObjects { GrantObjects::ExternalVolumes(objects) => { write!(f, "EXTERNAL VOLUME {}", display_comma_separated(objects)) } + GrantObjects::Procedure { name, arg_types } => { + write!(f, "PROCEDURE {name}")?; + if !arg_types.is_empty() { + write!(f, "({})", display_comma_separated(arg_types))?; + } + Ok(()) + } + GrantObjects::Function { name, arg_types } => { + write!(f, "FUNCTION {name}")?; + if !arg_types.is_empty() { + write!(f, "({})", display_comma_separated(arg_types))?; + } + Ok(()) + } + } + } +} + +/// A `DENY` statement +/// +/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/deny-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DenyStatement { + pub privileges: Privileges, + pub objects: GrantObjects, + pub grantees: Vec, + pub granted_by: Option, + pub cascade: Option, +} + +impl fmt::Display for DenyStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DENY {}", self.privileges)?; + write!(f, " ON {}", self.objects)?; + if !self.grantees.is_empty() { + write!(f, " TO {}", display_comma_separated(&self.grantees))?; } + if let Some(cascade) = &self.cascade { + write!(f, " {cascade}")?; + } + if let Some(granted_by) = &self.granted_by { + write!(f, " AS {granted_by}")?; + } + Ok(()) } } @@ -6705,7 +7455,7 @@ pub enum AssignmentTarget { impl fmt::Display for AssignmentTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - AssignmentTarget::ColumnName(column) => write!(f, "{}", column), + AssignmentTarget::ColumnName(column) => write!(f, "{column}"), AssignmentTarget::Tuple(columns) => write!(f, "({})", display_comma_separated(columns)), } } @@ -6829,6 +7579,65 @@ impl fmt::Display for CloseCursor { } } +/// A Drop Domain statement +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DropDomain { + /// Whether to drop the domain if it exists + pub if_exists: bool, + /// The name of the domain to drop + pub name: ObjectName, + /// The behavior to apply when dropping the domain + pub drop_behavior: Option, +} + +/// 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). +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TypedString { + pub data_type: DataType, + /// The value of the constant. + /// Hint: you can unwrap the string value using `value.into_string()`. + pub value: ValueWithSpan, + /// Flags whether this TypedString uses the [ODBC syntax]. + /// + /// Example: + /// ```sql + /// -- An ODBC date literal: + /// SELECT {d '2025-07-16'} + /// -- This is equivalent to the standard ANSI SQL literal: + /// SELECT DATE '2025-07-16' + /// + /// [ODBC syntax]: https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/date-time-and-timestamp-literals?view=sql-server-2017 + pub uses_odbc_syntax: bool, +} + +impl fmt::Display for TypedString { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let data_type = &self.data_type; + let value = &self.value; + match self.uses_odbc_syntax { + false => { + write!(f, "{data_type}")?; + write!(f, " {value}") + } + true => { + let prefix = match data_type { + DataType::Date => "d", + DataType::Time(..) => "t", + DataType::Timestamp(..) => "ts", + _ => "?", + }; + write!(f, "{{{prefix} {value}}}") + } + } + } +} + /// A function call #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -6905,7 +7714,8 @@ impl fmt::Display for Function { } if let Some(o) = &self.over { - write!(f, " OVER {o}")?; + f.write_str(" OVER ")?; + o.fmt(f)?; } if self.uses_odbc_syntax { @@ -6936,8 +7746,8 @@ impl fmt::Display for FunctionArguments { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FunctionArguments::None => Ok(()), - FunctionArguments::Subquery(query) => write!(f, "({})", query), - FunctionArguments::List(args) => write!(f, "({})", args), + FunctionArguments::Subquery(query) => write!(f, "({query})"), + FunctionArguments::List(args) => write!(f, "({args})"), } } } @@ -6958,7 +7768,7 @@ pub struct FunctionArgumentList { impl fmt::Display for FunctionArgumentList { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(duplicate_treatment) = self.duplicate_treatment { - write!(f, "{} ", duplicate_treatment)?; + write!(f, "{duplicate_treatment} ")?; } write!(f, "{}", display_comma_separated(&self.args))?; if !self.clauses.is_empty() { @@ -7018,7 +7828,7 @@ impl fmt::Display for FunctionArgumentClause { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FunctionArgumentClause::IgnoreOrRespectNulls(null_treatment) => { - write!(f, "{}", null_treatment) + write!(f, "{null_treatment}") } FunctionArgumentClause::OrderBy(order_by) => { write!(f, "ORDER BY {}", display_comma_separated(order_by)) @@ -7072,6 +7882,25 @@ impl fmt::Display for DuplicateTreatment { } } +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AnalyzeFormatKind { + /// e.g. `EXPLAIN ANALYZE FORMAT JSON SELECT * FROM tbl` + Keyword(AnalyzeFormat), + /// e.g. `EXPLAIN ANALYZE FORMAT=JSON SELECT * FROM tbl` + Assignment(AnalyzeFormat), +} + +impl fmt::Display for AnalyzeFormatKind { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + AnalyzeFormatKind::Keyword(format) => write!(f, "FORMAT {format}"), + AnalyzeFormatKind::Assignment(format) => write!(f, "FORMAT={format}"), + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7079,6 +7908,8 @@ pub enum AnalyzeFormat { TEXT, GRAPHVIZ, JSON, + TRADITIONAL, + TREE, } impl fmt::Display for AnalyzeFormat { @@ -7087,6 +7918,8 @@ impl fmt::Display for AnalyzeFormat { AnalyzeFormat::TEXT => "TEXT", AnalyzeFormat::GRAPHVIZ => "GRAPHVIZ", AnalyzeFormat::JSON => "JSON", + AnalyzeFormat::TRADITIONAL => "TRADITIONAL", + AnalyzeFormat::TREE => "TREE", }) } } @@ -7199,6 +8032,8 @@ pub enum ObjectType { Sequence, Stage, Type, + User, + Stream, } impl fmt::Display for ObjectType { @@ -7214,6 +8049,8 @@ impl fmt::Display for ObjectType { ObjectType::Sequence => "SEQUENCE", ObjectType::Stage => "STAGE", ObjectType::Type => "TYPE", + ObjectType::User => "USER", + ObjectType::Stream => "STREAM", }) } } @@ -7457,17 +8294,29 @@ pub enum SqlOption { range_direction: Option, for_values: Vec, }, + /// Comment parameter (supports `=` and no `=` syntax) + Comment(CommentDef), + /// MySQL TableSpace option + /// + TableSpace(TablespaceOption), + /// An option representing a key value pair, where the value is a parenthesized list and with an optional name + /// e.g. + /// + /// UNION = (tbl_name\[,tbl_name\]...) + /// ENGINE = ReplicatedMergeTree('/table_name','{replica}', ver) + /// ENGINE = SummingMergeTree(\[columns\]) + NamedParenthesizedList(NamedParenthesizedList), } impl fmt::Display for SqlOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - SqlOption::Clustered(c) => write!(f, "{}", c), + SqlOption::Clustered(c) => write!(f, "{c}"), SqlOption::Ident(ident) => { - write!(f, "{}", ident) + write!(f, "{ident}") } SqlOption::KeyValue { key: name, value } => { - write!(f, "{} = {}", name, value) + write!(f, "{name} = {value}") } SqlOption::Partition { column_name, @@ -7488,10 +8337,54 @@ impl fmt::Display for SqlOption { display_comma_separated(for_values) ) } + SqlOption::TableSpace(tablespace_option) => { + write!(f, "TABLESPACE {}", tablespace_option.name)?; + match tablespace_option.storage { + Some(StorageType::Disk) => write!(f, " STORAGE DISK"), + Some(StorageType::Memory) => write!(f, " STORAGE MEMORY"), + None => Ok(()), + } + } + SqlOption::Comment(comment) => match comment { + CommentDef::WithEq(comment) => { + write!(f, "COMMENT = '{comment}'") + } + CommentDef::WithoutEq(comment) => { + write!(f, "COMMENT '{comment}'") + } + }, + SqlOption::NamedParenthesizedList(value) => { + write!(f, "{} = ", value.key)?; + if let Some(key) = &value.name { + write!(f, "{key}")?; + } + if !value.values.is_empty() { + write!(f, "({})", display_comma_separated(&value.values))? + } + Ok(()) + } } } } +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum StorageType { + Disk, + Memory, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// MySql TableSpace option +/// +pub struct TablespaceOption { + pub name: String, + pub storage: Option, +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7506,6 +8399,70 @@ impl fmt::Display for SecretOption { } } +/// A `CREATE SERVER` statement. +/// +/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createserver.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateServerStatement { + pub name: ObjectName, + pub if_not_exists: bool, + pub server_type: Option, + pub version: Option, + pub foreign_data_wrapper: ObjectName, + pub options: Option>, +} + +impl fmt::Display for CreateServerStatement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let CreateServerStatement { + name, + if_not_exists, + server_type, + version, + foreign_data_wrapper, + options, + } = self; + + write!( + f, + "CREATE SERVER {if_not_exists}{name} ", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + + if let Some(st) = server_type { + write!(f, "TYPE {st} ")?; + } + + if let Some(v) = version { + write!(f, "VERSION {v} ")?; + } + + write!(f, "FOREIGN DATA WRAPPER {foreign_data_wrapper}")?; + + if let Some(o) = options { + write!(f, " OPTIONS ({o})", o = display_comma_separated(o))?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateServerOption { + pub key: Ident, + pub value: Ident, +} + +impl fmt::Display for CreateServerOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.key, self.value) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7520,7 +8477,7 @@ impl fmt::Display for AttachDuckDBDatabaseOption { AttachDuckDBDatabaseOption::ReadOnly(Some(true)) => write!(f, "READ_ONLY true"), AttachDuckDBDatabaseOption::ReadOnly(Some(false)) => write!(f, "READ_ONLY false"), AttachDuckDBDatabaseOption::ReadOnly(None) => write!(f, "READ_ONLY"), - AttachDuckDBDatabaseOption::Type(t) => write!(f, "TYPE {}", t), + AttachDuckDBDatabaseOption::Type(t) => write!(f, "TYPE {t}"), } } } @@ -7817,29 +8774,103 @@ impl fmt::Display for CopyOption { /// An option in `COPY` statement before PostgreSQL version 9.0. /// -/// +/// [PostgreSQL](https://www.postgresql.org/docs/8.4/sql-copy.html) +/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_COPY-alphabetical-parm-list.html) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum CopyLegacyOption { + /// ACCEPTANYDATE + AcceptAnyDate, + /// ACCEPTINVCHARS + AcceptInvChars(Option), /// BINARY Binary, + /// BLANKSASNULL + BlankAsNull, + /// CSV ... + Csv(Vec), + /// DATEFORMAT \[ AS \] {'dateformat_string' | 'auto' } + DateFormat(Option), /// DELIMITER \[ AS \] 'delimiter_character' Delimiter(char), + /// EMPTYASNULL + EmptyAsNull, + /// IAM_ROLE { DEFAULT | 'arn:aws:iam::123456789:role/role1' } + IamRole(IamRoleKind), + /// IGNOREHEADER \[ AS \] number_rows + IgnoreHeader(u64), /// NULL \[ AS \] 'null_string' Null(String), - /// CSV ... - Csv(Vec), + /// TIMEFORMAT \[ AS \] {'timeformat_string' | 'auto' | 'epochsecs' | 'epochmillisecs' } + TimeFormat(Option), + /// TRUNCATECOLUMNS + TruncateColumns, } impl fmt::Display for CopyLegacyOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use CopyLegacyOption::*; match self { + AcceptAnyDate => write!(f, "ACCEPTANYDATE"), + AcceptInvChars(ch) => { + write!(f, "ACCEPTINVCHARS")?; + if let Some(ch) = ch { + write!(f, " '{}'", value::escape_single_quote_string(ch))?; + } + Ok(()) + } Binary => write!(f, "BINARY"), + BlankAsNull => write!(f, "BLANKSASNULL"), + Csv(opts) => { + write!(f, "CSV")?; + if !opts.is_empty() { + write!(f, " {}", display_separated(opts, " "))?; + } + Ok(()) + } + DateFormat(fmt) => { + write!(f, "DATEFORMAT")?; + if let Some(fmt) = fmt { + write!(f, " '{}'", value::escape_single_quote_string(fmt))?; + } + Ok(()) + } Delimiter(char) => write!(f, "DELIMITER '{char}'"), + EmptyAsNull => write!(f, "EMPTYASNULL"), + IamRole(role) => write!(f, "IAM_ROLE {role}"), + IgnoreHeader(num_rows) => write!(f, "IGNOREHEADER {num_rows}"), Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)), - Csv(opts) => write!(f, "CSV {}", display_separated(opts, " ")), + TimeFormat(fmt) => { + write!(f, "TIMEFORMAT")?; + if let Some(fmt) = fmt { + write!(f, " '{}'", value::escape_single_quote_string(fmt))?; + } + Ok(()) + } + TruncateColumns => write!(f, "TRUNCATECOLUMNS"), + } + } +} + +/// An `IAM_ROLE` option in the AWS ecosystem +/// +/// [Redshift COPY](https://docs.aws.amazon.com/redshift/latest/dg/copy-parameters-authorization.html#copy-iam-role) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum IamRoleKind { + /// Default role + Default, + /// Specific role ARN, for example: `arn:aws:iam::123456789:role/role1` + Arn(String), +} + +impl fmt::Display for IamRoleKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + IamRoleKind::Default => write!(f, "DEFAULT"), + IamRoleKind::Arn(arn) => write!(f, "'{arn}'"), } } } @@ -8076,24 +9107,36 @@ impl Display for MergeClause { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct OutputClause { - pub select_items: Vec, - pub into_table: SelectInto, +pub enum OutputClause { + Output { + select_items: Vec, + into_table: Option, + }, + Returning { + select_items: Vec, + }, } impl fmt::Display for OutputClause { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let OutputClause { - select_items, - into_table, - } = self; - - write!( - f, - "OUTPUT {} {}", - display_comma_separated(select_items), - into_table - ) + match self { + OutputClause::Output { + select_items, + into_table, + } => { + f.write_str("OUTPUT ")?; + display_comma_separated(select_items).fmt(f)?; + if let Some(into_table) = into_table { + f.write_str(" ")?; + into_table.fmt(f)?; + } + Ok(()) + } + OutputClause::Returning { select_items } => { + f.write_str("RETURNING ")?; + display_comma_separated(select_items).fmt(f) + } + } } } @@ -8453,6 +9496,30 @@ pub enum CreateFunctionBody { /// /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html Return(Expr), + + /// Function body expression using the 'AS RETURN' keywords + /// + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(a INT, b INT) + /// RETURNS TABLE + /// AS RETURN (SELECT a + b AS sum); + /// ``` + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql + AsReturnExpr(Expr), + + /// Function body expression using the 'AS RETURN' keywords, with an un-parenthesized SELECT query + /// + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(a INT, b INT) + /// RETURNS TABLE + /// AS RETURN SELECT a + b AS sum; + /// ``` + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#select_stmt + AsReturnSelect(Select), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -8757,27 +9824,20 @@ impl Display for CreateViewParams { } } -/// Engine of DB. Some warehouse has parameters of engine, e.g. [ClickHouse] -/// -/// [ClickHouse]: https://clickhouse.com/docs/en/engines/table-engines #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct TableEngine { - pub name: String, - pub parameters: Option>, -} - -impl Display for TableEngine { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name)?; - - if let Some(parameters) = self.parameters.as_ref() { - write!(f, "({})", display_comma_separated(parameters))?; - } - - Ok(()) - } +/// Key/Value, where the value is a (optionally named) list of identifiers +/// +/// ```sql +/// UNION = (tbl_name[,tbl_name]...) +/// ENGINE = ReplicatedMergeTree('/table_name','{replica}', ver) +/// ENGINE = SummingMergeTree([columns]) +/// ``` +pub struct NamedParenthesizedList { + pub key: Ident, + pub name: Option, + pub values: Vec, } /// Snowflake `WITH ROW ACCESS POLICY policy_name ON (identifier, ...)` @@ -8816,12 +9876,12 @@ impl Display for RowAccessPolicy { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Tag { - pub key: Ident, + pub key: ObjectName, pub value: String, } impl Tag { - pub fn new(key: Ident, value: String) -> Self { + pub fn new(key: ObjectName, value: String) -> Self { Self { key, value } } } @@ -8832,6 +9892,23 @@ impl Display for Tag { } } +/// Snowflake `WITH CONTACT ( purpose = contact [ , purpose = contact ...] )` +/// +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ContactEntry { + pub purpose: String, + pub contact: String, +} + +impl Display for ContactEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} = {}", self.purpose, self.contact) + } +} + /// Helper to indicate if a comment includes the `=` in the display form #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -8841,18 +9918,12 @@ pub enum CommentDef { /// Does not include `=` when printing the comment, as `COMMENT 'comment'` WithEq(String), WithoutEq(String), - // For Hive dialect, the table comment is after the column definitions without `=`, - // so we need to add an extra variant to allow to identify this case when displaying. - // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) - AfterColumnDefsWithoutEq(String), } impl Display for CommentDef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - CommentDef::WithEq(comment) - | CommentDef::WithoutEq(comment) - | CommentDef::AfterColumnDefsWithoutEq(comment) => write!(f, "{comment}"), + CommentDef::WithEq(comment) | CommentDef::WithoutEq(comment) => write!(f, "{comment}"), } } } @@ -9032,10 +10103,36 @@ impl fmt::Display for ShowStatementIn { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.clause)?; if let Some(parent_type) = &self.parent_type { - write!(f, " {}", parent_type)?; + write!(f, " {parent_type}")?; } if let Some(parent_name) = &self.parent_name { - write!(f, " {}", parent_name)?; + write!(f, " {parent_name}")?; + } + Ok(()) + } +} + +/// A Show Charset statement +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ShowCharset { + /// The statement can be written as `SHOW CHARSET` or `SHOW CHARACTER SET` + /// true means CHARSET was used and false means CHARACTER SET was used + pub is_shorthand: bool, + pub filter: Option, +} + +impl fmt::Display for ShowCharset { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SHOW")?; + if self.is_shorthand { + write!(f, " CHARSET")?; + } else { + write!(f, " CHARACTER SET")?; + } + if self.filter.is_some() { + write!(f, " {}", self.filter.as_ref().unwrap())?; } Ok(()) } @@ -9116,7 +10213,7 @@ impl fmt::Display for TableObject { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::TableName(table_name) => write!(f, "{table_name}"), - Self::TableFunction(func) => write!(f, "FUNCTION {}", func), + Self::TableFunction(func) => write!(f, "FUNCTION {func}"), } } } @@ -9264,6 +10361,29 @@ impl Display for StorageSerializationPolicy { } } +/// Snowflake CatalogSyncNamespaceMode +/// ```sql +/// [ CATALOG_SYNC_NAMESPACE_MODE = { NEST | FLATTEN } ] +/// ``` +/// +/// +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CatalogSyncNamespaceMode { + Nest, + Flatten, +} + +impl Display for CatalogSyncNamespaceMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CatalogSyncNamespaceMode::Nest => write!(f, "NEST"), + CatalogSyncNamespaceMode::Flatten => write!(f, "FLATTEN"), + } + } +} + /// Variants of the Snowflake `COPY INTO` statement #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -9304,7 +10424,7 @@ pub struct ReturnStatement { impl fmt::Display for ReturnStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.value { - Some(ReturnStatementValue::Expr(expr)) => write!(f, "RETURN {}", expr), + Some(ReturnStatementValue::Expr(expr)) => write!(f, "RETURN {expr}"), None => write!(f, "RETURN"), } } @@ -9318,8 +10438,280 @@ pub enum ReturnStatementValue { Expr(Expr), } +/// Represents an `OPEN` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct OpenStatement { + /// Cursor name + pub cursor_name: Ident, +} + +impl fmt::Display for OpenStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "OPEN {}", self.cursor_name) + } +} + +/// Specifies Include / Exclude NULL within UNPIVOT command. +/// For example +/// `UNPIVOT (column1 FOR new_column IN (col3, col4, col5, col6))` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum NullInclusion { + IncludeNulls, + ExcludeNulls, +} + +impl fmt::Display for NullInclusion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NullInclusion::IncludeNulls => write!(f, "INCLUDE NULLS"), + NullInclusion::ExcludeNulls => write!(f, "EXCLUDE NULLS"), + } + } +} + +/// Checks membership of a value in a JSON array +/// +/// Syntax: +/// ```sql +/// MEMBER OF() +/// ``` +/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/json-search-functions.html#operator_member-of) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct MemberOf { + pub value: Box, + pub array: Box, +} + +impl fmt::Display for MemberOf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} MEMBER OF({})", self.value, self.array) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ExportData { + pub options: Vec, + pub query: Box, + pub connection: Option, +} + +impl fmt::Display for ExportData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(connection) = &self.connection { + write!( + f, + "EXPORT DATA WITH CONNECTION {connection} OPTIONS({}) AS {}", + display_comma_separated(&self.options), + self.query + ) + } else { + write!( + f, + "EXPORT DATA OPTIONS({}) AS {}", + display_comma_separated(&self.options), + self.query + ) + } + } +} +/// Creates a user +/// +/// Syntax: +/// ```sql +/// CREATE [OR REPLACE] USER [IF NOT EXISTS] [OPTIONS] +/// ``` +/// +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-user) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateUser { + pub or_replace: bool, + pub if_not_exists: bool, + pub name: Ident, + pub options: KeyValueOptions, + pub with_tags: bool, + pub tags: KeyValueOptions, +} + +impl fmt::Display for CreateUser { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE")?; + if self.or_replace { + write!(f, " OR REPLACE")?; + } + write!(f, " USER")?; + if self.if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " {}", self.name)?; + if !self.options.options.is_empty() { + write!(f, " {}", self.options)?; + } + if !self.tags.options.is_empty() { + if self.with_tags { + write!(f, " WITH")?; + } + write!(f, " TAG ({})", self.tags)?; + } + Ok(()) + } +} + +/// Specifies how to create a new table based on an existing table's schema. +/// '''sql +/// CREATE TABLE new LIKE old ... +/// ''' +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateTableLikeKind { + /// '''sql + /// CREATE TABLE new (LIKE old ...) + /// ''' + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_NEW.html) + Parenthesized(CreateTableLike), + /// '''sql + /// CREATE TABLE new LIKE old ... + /// ''' + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-table#label-create-table-like) + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_table_like) + Plain(CreateTableLike), +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateTableLikeDefaults { + Including, + Excluding, +} + +impl fmt::Display for CreateTableLikeDefaults { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreateTableLikeDefaults::Including => write!(f, "INCLUDING DEFAULTS"), + CreateTableLikeDefaults::Excluding => write!(f, "EXCLUDING DEFAULTS"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateTableLike { + pub name: ObjectName, + pub defaults: Option, +} + +impl fmt::Display for CreateTableLike { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "LIKE {}", self.name)?; + if let Some(defaults) = &self.defaults { + write!(f, " {defaults}")?; + } + Ok(()) + } +} + +/// Specifies the refresh mode for the dynamic table. +/// +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-dynamic-table) +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RefreshModeKind { + Auto, + Full, + Incremental, +} + +impl fmt::Display for RefreshModeKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RefreshModeKind::Auto => write!(f, "AUTO"), + RefreshModeKind::Full => write!(f, "FULL"), + RefreshModeKind::Incremental => write!(f, "INCREMENTAL"), + } + } +} + +/// Specifies the behavior of the initial refresh of the dynamic table. +/// +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-dynamic-table) +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum InitializeKind { + OnCreate, + OnSchedule, +} + +impl fmt::Display for InitializeKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + InitializeKind::OnCreate => write!(f, "ON_CREATE"), + InitializeKind::OnSchedule => write!(f, "ON_SCHEDULE"), + } + } +} + +/// Re-sorts rows and reclaims space in either a specified table or all tables in the current database +/// +/// '''sql +/// VACUUM [ FULL | SORT ONLY | DELETE ONLY | REINDEX | RECLUSTER ] [ \[ table_name \] [ TO threshold PERCENT ] \[ BOOST \] ] +/// ''' +/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct VacuumStatement { + pub full: bool, + pub sort_only: bool, + pub delete_only: bool, + pub reindex: bool, + pub recluster: bool, + pub table_name: Option, + pub threshold: Option, + pub boost: bool, +} + +impl fmt::Display for VacuumStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "VACUUM{}{}{}{}{}", + if self.full { " FULL" } else { "" }, + if self.sort_only { " SORT ONLY" } else { "" }, + if self.delete_only { " DELETE ONLY" } else { "" }, + if self.reindex { " REINDEX" } else { "" }, + if self.recluster { " RECLUSTER" } else { "" }, + )?; + if let Some(table_name) = &self.table_name { + write!(f, " {table_name}")?; + } + if let Some(threshold) = &self.threshold { + write!(f, " TO {threshold} PERCENT")?; + } + if self.boost { + write!(f, " BOOST")?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { + use crate::tokenizer::Location; + use super::*; #[test] @@ -9615,4 +11007,16 @@ mod tests { test_steps(OneOrManyWithParens::Many(vec![2]), vec![2], 3); test_steps(OneOrManyWithParens::Many(vec![3, 4]), vec![3, 4], 4); } + + // Tests that the position in the code of an `Ident` does not affect its + // ordering. + #[test] + fn test_ident_ord() { + let mut a = Ident::with_span(Span::new(Location::new(1, 1), Location::new(1, 1)), "a"); + let mut b = Ident::with_span(Span::new(Location::new(2, 2), Location::new(2, 2)), "b"); + + assert!(a < b); + std::mem::swap(&mut a.span, &mut b.span); + assert!(a < b); + } } diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 73fe9cf42..d0bb05e3c 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -139,6 +139,11 @@ pub enum BinaryOperator { DuckIntegerDivide, /// MySQL [`DIV`](https://dev.mysql.com/doc/refman/8.0/en/arithmetic-functions.html) integer division MyIntegerDivide, + /// MATCH operator, e.g. `a MATCH b` (SQLite-specific) + /// See + Match, + /// REGEXP operator, e.g. `a REGEXP b` (SQLite-specific) + Regexp, /// Support for custom operators (such as Postgres custom operators) Custom(String), /// Bitwise XOR, e.g. `a # b` (PostgreSQL-specific) @@ -350,6 +355,8 @@ impl fmt::Display for BinaryOperator { BinaryOperator::BitwiseXor => f.write_str("^"), BinaryOperator::DuckIntegerDivide => f.write_str("//"), BinaryOperator::MyIntegerDivide => f.write_str("DIV"), + BinaryOperator::Match => f.write_str("MATCH"), + BinaryOperator::Regexp => f.write_str("REGEXP"), BinaryOperator::Custom(s) => f.write_str(s), BinaryOperator::PGBitwiseXor => f.write_str("#"), BinaryOperator::PGBitwiseShiftLeft => f.write_str("<<"), diff --git a/src/ast/query.rs b/src/ast/query.rs index 982985ec3..967af85c2 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -27,6 +27,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::{ ast::*, + display_utils::{indented_list, SpaceOrNewline}, tokenizer::{Token, TokenWithSpan}, }; @@ -62,35 +63,49 @@ pub struct Query { /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/format) /// (ClickHouse-specific) pub format_clause: Option, + + /// Pipe operator + pub pipe_operators: Vec, } impl fmt::Display for Query { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(ref with) = self.with { - write!(f, "{with} ")?; + with.fmt(f)?; + SpaceOrNewline.fmt(f)?; } - write!(f, "{}", self.body)?; + self.body.fmt(f)?; if let Some(ref order_by) = self.order_by { - write!(f, " {order_by}")?; + f.write_str(" ")?; + order_by.fmt(f)?; } if let Some(ref limit_clause) = self.limit_clause { limit_clause.fmt(f)?; } if let Some(ref settings) = self.settings { - write!(f, " SETTINGS {}", display_comma_separated(settings))?; + f.write_str(" SETTINGS ")?; + display_comma_separated(settings).fmt(f)?; } if let Some(ref fetch) = self.fetch { - write!(f, " {fetch}")?; + f.write_str(" ")?; + fetch.fmt(f)?; } if !self.locks.is_empty() { - write!(f, " {}", display_separated(&self.locks, " "))?; + f.write_str(" ")?; + display_separated(&self.locks, " ").fmt(f)?; } if let Some(ref for_clause) = self.for_clause { - write!(f, " {}", for_clause)?; + f.write_str(" ")?; + for_clause.fmt(f)?; } if let Some(ref format) = self.format_clause { - write!(f, " {}", format)?; + f.write_str(" ")?; + format.fmt(f)?; + } + for pipe_operator in &self.pipe_operators { + f.write_str(" |> ")?; + pipe_operator.fmt(f)?; } Ok(()) } @@ -146,6 +161,7 @@ pub enum SetExpr { Insert(Statement), Update(Statement), Delete(Statement), + Merge(Statement), Table(Box), } @@ -163,29 +179,40 @@ impl SetExpr { impl fmt::Display for SetExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - SetExpr::Select(s) => write!(f, "{s}"), - SetExpr::Query(q) => write!(f, "({q})"), - SetExpr::Values(v) => write!(f, "{v}"), - SetExpr::Insert(v) => write!(f, "{v}"), - SetExpr::Update(v) => write!(f, "{v}"), - SetExpr::Delete(v) => write!(f, "{v}"), - SetExpr::Table(t) => write!(f, "{t}"), + SetExpr::Select(s) => s.fmt(f), + SetExpr::Query(q) => { + f.write_str("(")?; + q.fmt(f)?; + f.write_str(")") + } + SetExpr::Values(v) => v.fmt(f), + SetExpr::Insert(v) => v.fmt(f), + SetExpr::Update(v) => v.fmt(f), + SetExpr::Delete(v) => v.fmt(f), + SetExpr::Merge(v) => v.fmt(f), + SetExpr::Table(t) => t.fmt(f), SetExpr::SetOperation { left, right, op, set_quantifier, } => { - write!(f, "{left} {op}")?; + left.fmt(f)?; + SpaceOrNewline.fmt(f)?; + op.fmt(f)?; match set_quantifier { SetQuantifier::All | SetQuantifier::Distinct | SetQuantifier::ByName | SetQuantifier::AllByName - | SetQuantifier::DistinctByName => write!(f, " {set_quantifier}")?, - SetQuantifier::None => write!(f, "{set_quantifier}")?, + | SetQuantifier::DistinctByName => { + f.write_str(" ")?; + set_quantifier.fmt(f)?; + } + SetQuantifier::None => {} } - write!(f, " {right}")?; + SpaceOrNewline.fmt(f)?; + right.fmt(f)?; Ok(()) } } @@ -236,7 +263,7 @@ impl fmt::Display for SetQuantifier { SetQuantifier::ByName => write!(f, "BY NAME"), SetQuantifier::AllByName => write!(f, "ALL BY NAME"), SetQuantifier::DistinctByName => write!(f, "DISTINCT BY NAME"), - SetQuantifier::None => write!(f, ""), + SetQuantifier::None => Ok(()), } } } @@ -296,6 +323,11 @@ pub struct Select { pub top_before_distinct: bool, /// projection expressions pub projection: Vec, + /// Excluded columns from the projection expression which are not specified + /// directly after a wildcard. + /// + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html) + pub exclude: Option, /// INTO pub into: Option, /// FROM @@ -316,7 +348,7 @@ pub struct Select { /// DISTRIBUTE BY (Hive) pub distribute_by: Vec, /// SORT BY (Hive) - pub sort_by: Vec, + pub sort_by: Vec, /// HAVING pub having: Option, /// WINDOW AS @@ -351,90 +383,126 @@ impl fmt::Display for Select { } if let Some(value_table_mode) = self.value_table_mode { - write!(f, " {value_table_mode}")?; + f.write_str(" ")?; + value_table_mode.fmt(f)?; } if let Some(ref top) = self.top { if self.top_before_distinct { - write!(f, " {top}")?; + f.write_str(" ")?; + top.fmt(f)?; } } if let Some(ref distinct) = self.distinct { - write!(f, " {distinct}")?; + f.write_str(" ")?; + distinct.fmt(f)?; } if let Some(ref top) = self.top { if !self.top_before_distinct { - write!(f, " {top}")?; + f.write_str(" ")?; + top.fmt(f)?; } } if !self.projection.is_empty() { - write!(f, " {}", display_comma_separated(&self.projection))?; + indented_list(f, &self.projection)?; + } + + if let Some(exclude) = &self.exclude { + write!(f, " {exclude}")?; } if let Some(ref into) = self.into { - write!(f, " {into}")?; + f.write_str(" ")?; + into.fmt(f)?; } if self.flavor == SelectFlavor::Standard && !self.from.is_empty() { - write!(f, " FROM {}", display_comma_separated(&self.from))?; + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, &self.from)?; } if !self.lateral_views.is_empty() { for lv in &self.lateral_views { - write!(f, "{lv}")?; + lv.fmt(f)?; } } if let Some(ref prewhere) = self.prewhere { - write!(f, " PREWHERE {prewhere}")?; + f.write_str(" PREWHERE ")?; + prewhere.fmt(f)?; } if let Some(ref selection) = self.selection { - write!(f, " WHERE {selection}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("WHERE")?; + SpaceOrNewline.fmt(f)?; + Indent(selection).fmt(f)?; } match &self.group_by { - GroupByExpr::All(_) => write!(f, " {}", self.group_by)?, + GroupByExpr::All(_) => { + SpaceOrNewline.fmt(f)?; + self.group_by.fmt(f)?; + } GroupByExpr::Expressions(exprs, _) => { if !exprs.is_empty() { - write!(f, " {}", self.group_by)? + SpaceOrNewline.fmt(f)?; + self.group_by.fmt(f)?; } } } if !self.cluster_by.is_empty() { - write!( - f, - " CLUSTER BY {}", - display_comma_separated(&self.cluster_by) - )?; + SpaceOrNewline.fmt(f)?; + f.write_str("CLUSTER BY")?; + SpaceOrNewline.fmt(f)?; + Indent(display_comma_separated(&self.cluster_by)).fmt(f)?; } if !self.distribute_by.is_empty() { - write!( - f, - " DISTRIBUTE BY {}", - display_comma_separated(&self.distribute_by) - )?; + SpaceOrNewline.fmt(f)?; + f.write_str("DISTRIBUTE BY")?; + SpaceOrNewline.fmt(f)?; + display_comma_separated(&self.distribute_by).fmt(f)?; } if !self.sort_by.is_empty() { - write!(f, " SORT BY {}", display_comma_separated(&self.sort_by))?; + SpaceOrNewline.fmt(f)?; + f.write_str("SORT BY")?; + SpaceOrNewline.fmt(f)?; + Indent(display_comma_separated(&self.sort_by)).fmt(f)?; } if let Some(ref having) = self.having { - write!(f, " HAVING {having}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("HAVING")?; + SpaceOrNewline.fmt(f)?; + Indent(having).fmt(f)?; } if self.window_before_qualify { if !self.named_window.is_empty() { - write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?; + SpaceOrNewline.fmt(f)?; + f.write_str("WINDOW")?; + SpaceOrNewline.fmt(f)?; + display_comma_separated(&self.named_window).fmt(f)?; } if let Some(ref qualify) = self.qualify { - write!(f, " QUALIFY {qualify}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("QUALIFY")?; + SpaceOrNewline.fmt(f)?; + qualify.fmt(f)?; } } else { if let Some(ref qualify) = self.qualify { - write!(f, " QUALIFY {qualify}")?; + SpaceOrNewline.fmt(f)?; + f.write_str("QUALIFY")?; + SpaceOrNewline.fmt(f)?; + qualify.fmt(f)?; } if !self.named_window.is_empty() { - write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?; + SpaceOrNewline.fmt(f)?; + f.write_str("WINDOW")?; + SpaceOrNewline.fmt(f)?; + display_comma_separated(&self.named_window).fmt(f)?; } } if let Some(ref connect_by) = self.connect_by { - write!(f, " {connect_by}")?; + SpaceOrNewline.fmt(f)?; + connect_by.fmt(f)?; } Ok(()) } @@ -540,12 +608,12 @@ pub struct With { impl fmt::Display for With { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "WITH {}{}", - if self.recursive { "RECURSIVE " } else { "" }, - display_comma_separated(&self.cte_tables) - ) + f.write_str("WITH ")?; + if self.recursive { + f.write_str("RECURSIVE ")?; + } + display_comma_separated(&self.cte_tables).fmt(f)?; + Ok(()) } } @@ -592,8 +660,24 @@ pub struct Cte { impl fmt::Display for Cte { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.materialized.as_ref() { - None => write!(f, "{} AS ({})", self.alias, self.query)?, - Some(materialized) => write!(f, "{} AS {materialized} ({})", self.alias, self.query)?, + None => { + self.alias.fmt(f)?; + f.write_str(" AS (")?; + NewLine.fmt(f)?; + Indent(&self.query).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")")?; + } + Some(materialized) => { + self.alias.fmt(f)?; + f.write_str(" AS ")?; + materialized.fmt(f)?; + f.write_str(" (")?; + NewLine.fmt(f)?; + Indent(&self.query).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")")?; + } }; if let Some(ref fr) = self.from { write!(f, " FROM {fr}")?; @@ -906,18 +990,21 @@ impl fmt::Display for ReplaceSelectElement { impl fmt::Display for SelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use core::fmt::Write; match &self { - SelectItem::UnnamedExpr(expr) => write!(f, "{expr}"), - SelectItem::ExprWithAlias { expr, alias } => write!(f, "{expr} AS {alias}"), + SelectItem::UnnamedExpr(expr) => expr.fmt(f), + SelectItem::ExprWithAlias { expr, alias } => { + expr.fmt(f)?; + f.write_str(" AS ")?; + alias.fmt(f) + } SelectItem::QualifiedWildcard(kind, additional_options) => { - write!(f, "{kind}")?; - write!(f, "{additional_options}")?; - Ok(()) + kind.fmt(f)?; + additional_options.fmt(f) } SelectItem::Wildcard(additional_options) => { - write!(f, "*")?; - write!(f, "{additional_options}")?; - Ok(()) + f.write_char('*')?; + additional_options.fmt(f) } } } @@ -933,9 +1020,10 @@ pub struct TableWithJoins { impl fmt::Display for TableWithJoins { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.relation)?; + self.relation.fmt(f)?; for join in &self.joins { - write!(f, "{join}")?; + SpaceOrNewline.fmt(f)?; + join.fmt(f)?; } Ok(()) } @@ -970,7 +1058,7 @@ impl fmt::Display for ConnectBy { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Setting { pub key: Ident, - pub value: Value, + pub value: Expr, } impl fmt::Display for Setting { @@ -1004,6 +1092,26 @@ impl fmt::Display for ExprWithAlias { } } +/// An expression optionally followed by an alias and order by options. +/// +/// Example: +/// ```sql +/// 42 AS myint ASC +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ExprWithAliasAndOrderBy { + pub expr: ExprWithAlias, + pub order_by: OrderByOptions, +} + +impl fmt::Display for ExprWithAliasAndOrderBy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", self.expr, self.order_by) + } +} + /// Arguments to a table-valued function #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1086,7 +1194,7 @@ impl fmt::Display for TableIndexHints { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {} ", self.hint_type, self.index_type)?; if let Some(for_clause) = &self.for_clause { - write!(f, "FOR {} ", for_clause)?; + write!(f, "FOR {for_clause} ")?; } write!(f, "({})", display_comma_separated(&self.index_names)) } @@ -1230,7 +1338,7 @@ pub enum TableFactor { Pivot { table: Box, aggregate_functions: Vec, // Function expression - value_column: Vec, + value_column: Vec, value_source: PivotValueSource, default_on_null: Option, alias: Option, @@ -1239,15 +1347,17 @@ pub enum TableFactor { /// /// Syntax: /// ```sql - /// table UNPIVOT(value FOR name IN (column1, [ column2, ... ])) [ alias ] + /// table UNPIVOT [ { INCLUDE | EXCLUDE } NULLS ] (value FOR name IN (column1, [ column2, ... ])) [ alias ] /// ``` /// /// See . + /// See . Unpivot { table: Box, - value: Ident, + value: Expr, name: Ident, - columns: Vec, + columns: Vec, + null_inclusion: Option, alias: Option, }, /// A `MATCH_RECOGNIZE` operation on a table. @@ -1302,13 +1412,37 @@ pub enum TableFactor { /// The alias for the table. alias: Option, }, + /// Snowflake's SEMANTIC_VIEW function for semantic models. + /// + /// + /// + /// ```sql + /// SELECT * FROM SEMANTIC_VIEW( + /// tpch_analysis + /// DIMENSIONS customer.customer_market_segment + /// METRICS orders.order_average_value + /// ); + /// ``` + SemanticView { + /// The name of the semantic model + name: ObjectName, + /// List of dimensions or expression referring to dimensions (e.g. DATE_PART('year', col)) + dimensions: Vec, + /// List of metrics (references to objects like orders.value, value, orders.*) + metrics: Vec, + /// List of facts or expressions referring to facts or dimensions. + facts: Vec, + /// WHERE clause for filtering + where_clause: Option, + /// The alias for the table + alias: Option, + }, } /// The table sample modifier options #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] - pub enum TableSampleKind { /// Table sample located before the table alias option BeforeTableAlias(Box), @@ -1362,7 +1496,7 @@ impl fmt::Display for TableSampleQuantity { } write!(f, "{}", self.value)?; if let Some(unit) = &self.unit { - write!(f, " {}", unit)?; + write!(f, " {unit}")?; } if self.parenthesized { write!(f, ")")?; @@ -1455,28 +1589,28 @@ impl fmt::Display for TableSampleBucket { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "BUCKET {} OUT OF {}", self.bucket, self.total)?; if let Some(on) = &self.on { - write!(f, " ON {}", on)?; + write!(f, " ON {on}")?; } Ok(()) } } impl fmt::Display for TableSample { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, " {}", self.modifier)?; + write!(f, "{}", self.modifier)?; if let Some(name) = &self.name { - write!(f, " {}", name)?; + write!(f, " {name}")?; } if let Some(quantity) = &self.quantity { - write!(f, " {}", quantity)?; + write!(f, " {quantity}")?; } if let Some(seed) = &self.seed { - write!(f, " {}", seed)?; + write!(f, " {seed}")?; } if let Some(bucket) = &self.bucket { - write!(f, " ({})", bucket)?; + write!(f, " ({bucket})")?; } if let Some(offset) = &self.offset { - write!(f, " OFFSET {}", offset)?; + write!(f, " OFFSET {offset}")?; } Ok(()) } @@ -1554,7 +1688,7 @@ impl fmt::Display for RowsPerMatch { RowsPerMatch::AllRows(mode) => { write!(f, "ALL ROWS PER MATCH")?; if let Some(mode) = mode { - write!(f, " {}", mode)?; + write!(f, " {mode}")?; } Ok(()) } @@ -1680,7 +1814,7 @@ impl fmt::Display for MatchRecognizePattern { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use MatchRecognizePattern::*; match self { - Symbol(symbol) => write!(f, "{}", symbol), + Symbol(symbol) => write!(f, "{symbol}"), Exclude(symbol) => write!(f, "{{- {symbol} -}}"), Permute(symbols) => write!(f, "PERMUTE({})", display_comma_separated(symbols)), Concat(patterns) => write!(f, "{}", display_separated(patterns, " ")), @@ -1743,9 +1877,9 @@ impl fmt::Display for TableFactor { sample, index_hints, } => { - write!(f, "{name}")?; + name.fmt(f)?; if let Some(json_path) = json_path { - write!(f, "{json_path}")?; + json_path.fmt(f)?; } if !partitions.is_empty() { write!(f, "PARTITION ({})", display_comma_separated(partitions))?; @@ -1765,7 +1899,7 @@ impl fmt::Display for TableFactor { write!(f, " WITH ORDINALITY")?; } if let Some(TableSampleKind::BeforeTableAlias(sample)) = sample { - write!(f, "{sample}")?; + write!(f, " {sample}")?; } if let Some(alias) = alias { write!(f, " AS {alias}")?; @@ -1777,10 +1911,10 @@ impl fmt::Display for TableFactor { write!(f, " WITH ({})", display_comma_separated(with_hints))?; } if let Some(version) = version { - write!(f, "{version}")?; + write!(f, " {version}")?; } if let Some(TableSampleKind::AfterTableAlias(sample)) = sample { - write!(f, "{sample}")?; + write!(f, " {sample}")?; } Ok(()) } @@ -1792,7 +1926,11 @@ impl fmt::Display for TableFactor { if *lateral { write!(f, "LATERAL ")?; } - write!(f, "({subquery})")?; + f.write_str("(")?; + NewLine.fmt(f)?; + Indent(subquery).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")")?; if let Some(alias) = alias { write!(f, " AS {alias}")?; } @@ -1900,10 +2038,15 @@ impl fmt::Display for TableFactor { } => { write!( f, - "{table} PIVOT({} FOR {} IN ({value_source})", + "{table} PIVOT({} FOR ", display_comma_separated(aggregate_functions), - Expr::CompoundIdentifier(value_column.to_vec()), )?; + if value_column.len() == 1 { + write!(f, "{}", value_column[0])?; + } else { + write!(f, "({})", display_comma_separated(value_column))?; + } + write!(f, " IN ({value_source})")?; if let Some(expr) = default_on_null { write!(f, " DEFAULT ON NULL ({expr})")?; } @@ -1915,15 +2058,19 @@ impl fmt::Display for TableFactor { } TableFactor::Unpivot { table, + null_inclusion, value, name, columns, alias, } => { + write!(f, "{table} UNPIVOT")?; + if let Some(null_inclusion) = null_inclusion { + write!(f, " {null_inclusion} ")?; + } write!( f, - "{} UNPIVOT({} FOR {} IN ({}))", - table, + "({} FOR {} IN ({}))", value, name, display_comma_separated(columns) @@ -1992,6 +2139,40 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::SemanticView { + name, + dimensions, + metrics, + facts, + where_clause, + alias, + } => { + write!(f, "SEMANTIC_VIEW({name}")?; + + if !dimensions.is_empty() { + write!(f, " DIMENSIONS {}", display_comma_separated(dimensions))?; + } + + if !metrics.is_empty() { + write!(f, " METRICS {}", display_comma_separated(metrics))?; + } + + if !facts.is_empty() { + write!(f, " FACTS {}", display_comma_separated(facts))?; + } + + if let Some(where_clause) = where_clause { + write!(f, " WHERE {where_clause}")?; + } + + write!(f, ")")?; + + if let Some(alias) = alias { + write!(f, " AS {alias}")?; + } + + Ok(()) + } } } } @@ -2043,7 +2224,7 @@ impl fmt::Display for TableAliasColumnDef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; if let Some(ref data_type) = self.data_type { - write!(f, " {}", data_type)?; + write!(f, " {data_type}")?; } Ok(()) } @@ -2064,8 +2245,8 @@ pub enum TableVersion { impl Display for TableVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - TableVersion::ForSystemTimeAsOf(e) => write!(f, " FOR SYSTEM_TIME AS OF {e}")?, - TableVersion::Function(func) => write!(f, " {func}")?, + TableVersion::ForSystemTimeAsOf(e) => write!(f, "FOR SYSTEM_TIME AS OF {e}")?, + TableVersion::Function(func) => write!(f, "{func}")?, } Ok(()) } @@ -2106,116 +2287,104 @@ impl fmt::Display for Join { Suffix(constraint) } if self.global { - write!(f, " GLOBAL")?; + write!(f, "GLOBAL ")?; } match &self.join_operator { - JoinOperator::Join(constraint) => write!( - f, - " {}JOIN {}{}", + JoinOperator::Join(constraint) => f.write_fmt(format_args!( + "{}JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::Inner(constraint) => write!( - f, - " {}INNER JOIN {}{}", + )), + JoinOperator::Inner(constraint) => f.write_fmt(format_args!( + "{}INNER JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::Left(constraint) => write!( - f, - " {}LEFT JOIN {}{}", + )), + JoinOperator::Left(constraint) => f.write_fmt(format_args!( + "{}LEFT JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::LeftOuter(constraint) => write!( - f, - " {}LEFT OUTER JOIN {}{}", + )), + JoinOperator::LeftOuter(constraint) => f.write_fmt(format_args!( + "{}LEFT OUTER JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::Right(constraint) => write!( - f, - " {}RIGHT JOIN {}{}", + )), + JoinOperator::Right(constraint) => f.write_fmt(format_args!( + "{}RIGHT JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::RightOuter(constraint) => write!( - f, - " {}RIGHT OUTER JOIN {}{}", + )), + JoinOperator::RightOuter(constraint) => f.write_fmt(format_args!( + "{}RIGHT OUTER JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::FullOuter(constraint) => write!( - f, - " {}FULL JOIN {}{}", + )), + JoinOperator::FullOuter(constraint) => f.write_fmt(format_args!( + "{}FULL JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::CrossJoin => write!(f, " CROSS JOIN {}", self.relation), - JoinOperator::Semi(constraint) => write!( - f, - " {}SEMI JOIN {}{}", + )), + JoinOperator::CrossJoin => f.write_fmt(format_args!("CROSS JOIN {}", self.relation)), + JoinOperator::Semi(constraint) => f.write_fmt(format_args!( + "{}SEMI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::LeftSemi(constraint) => write!( - f, - " {}LEFT SEMI JOIN {}{}", + )), + JoinOperator::LeftSemi(constraint) => f.write_fmt(format_args!( + "{}LEFT SEMI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::RightSemi(constraint) => write!( - f, - " {}RIGHT SEMI JOIN {}{}", + )), + JoinOperator::RightSemi(constraint) => f.write_fmt(format_args!( + "{}RIGHT SEMI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::Anti(constraint) => write!( - f, - " {}ANTI JOIN {}{}", + )), + JoinOperator::Anti(constraint) => f.write_fmt(format_args!( + "{}ANTI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::LeftAnti(constraint) => write!( - f, - " {}LEFT ANTI JOIN {}{}", + )), + JoinOperator::LeftAnti(constraint) => f.write_fmt(format_args!( + "{}LEFT ANTI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::RightAnti(constraint) => write!( - f, - " {}RIGHT ANTI JOIN {}{}", + )), + JoinOperator::RightAnti(constraint) => f.write_fmt(format_args!( + "{}RIGHT ANTI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::CrossApply => write!(f, " CROSS APPLY {}", self.relation), - JoinOperator::OuterApply => write!(f, " OUTER APPLY {}", self.relation), + )), + JoinOperator::CrossApply => f.write_fmt(format_args!("CROSS APPLY {}", self.relation)), + JoinOperator::OuterApply => f.write_fmt(format_args!("OUTER APPLY {}", self.relation)), JoinOperator::AsOf { match_condition, constraint, - } => write!( - f, - " ASOF JOIN {} MATCH_CONDITION ({match_condition}){}", + } => f.write_fmt(format_args!( + "ASOF JOIN {} MATCH_CONDITION ({match_condition}){}", self.relation, suffix(constraint) - ), - JoinOperator::StraightJoin(constraint) => { - write!(f, " STRAIGHT_JOIN {}{}", self.relation, suffix(constraint)) - } + )), + JoinOperator::StraightJoin(constraint) => f.write_fmt(format_args!( + "STRAIGHT_JOIN {}{}", + self.relation, + suffix(constraint) + )), } } } @@ -2305,7 +2474,7 @@ impl fmt::Display for OrderBy { write!(f, " {}", display_comma_separated(exprs))?; } OrderByKind::All(all) => { - write!(f, " ALL{}", all)?; + write!(f, " ALL{all}")?; } } @@ -2336,7 +2505,7 @@ impl fmt::Display for OrderByExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", self.expr, self.options)?; if let Some(ref with_fill) = self.with_fill { - write!(f, " {}", with_fill)? + write!(f, " {with_fill}")? } Ok(()) } @@ -2359,13 +2528,13 @@ impl fmt::Display for WithFill { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "WITH FILL")?; if let Some(ref from) = self.from { - write!(f, " FROM {}", from)?; + write!(f, " FROM {from}")?; } if let Some(ref to) = self.to { - write!(f, " TO {}", to)?; + write!(f, " TO {to}")?; } if let Some(ref step) = self.step { - write!(f, " STEP {}", step)?; + write!(f, " STEP {step}")?; } Ok(()) } @@ -2394,7 +2563,7 @@ impl fmt::Display for InterpolateExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.column)?; if let Some(ref expr) = self.expr { - write!(f, " AS {}", expr)?; + write!(f, " AS {expr}")?; } Ok(()) } @@ -2472,7 +2641,7 @@ impl fmt::Display for LimitClause { Ok(()) } LimitClause::OffsetCommaLimit { offset, limit } => { - write!(f, " LIMIT {}, {}", offset, limit) + write!(f, " LIMIT {offset}, {limit}") } } } @@ -2513,6 +2682,296 @@ impl fmt::Display for OffsetRows { } } +/// Pipe syntax, first introduced in Google BigQuery. +/// Example: +/// +/// ```sql +/// FROM Produce +/// |> WHERE sales > 0 +/// |> AGGREGATE SUM(sales) AS total_sales, COUNT(*) AS num_sales +/// GROUP BY item; +/// ``` +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum PipeOperator { + /// Limits the number of rows to return in a query, with an optional OFFSET clause to skip over rows. + /// + /// Syntax: `|> LIMIT [OFFSET ]` + /// + /// See more at + Limit { expr: Expr, offset: Option }, + /// Filters the results of the input table. + /// + /// Syntax: `|> WHERE ` + /// + /// See more at + Where { expr: Expr }, + /// `ORDER BY [ASC|DESC], ...` + OrderBy { exprs: Vec }, + /// Produces a new table with the listed columns, similar to the outermost SELECT clause in a table subquery in standard syntax. + /// + /// Syntax `|> SELECT [[AS] alias], ...` + /// + /// See more at + Select { exprs: Vec }, + /// Propagates the existing table and adds computed columns, similar to SELECT *, new_column in standard syntax. + /// + /// Syntax: `|> EXTEND [[AS] alias], ...` + /// + /// See more at + Extend { exprs: Vec }, + /// Replaces the value of a column in the current table, similar to SELECT * REPLACE (expression AS column) in standard syntax. + /// + /// Syntax: `|> SET = , ...` + /// + /// See more at + Set { assignments: Vec }, + /// Removes listed columns from the current table, similar to SELECT * EXCEPT (column) in standard syntax. + /// + /// Syntax: `|> DROP , ...` + /// + /// See more at + Drop { columns: Vec }, + /// Introduces a table alias for the input table, similar to applying the AS alias clause on a table subquery in standard syntax. + /// + /// Syntax: `|> AS ` + /// + /// See more at + As { alias: Ident }, + /// Performs aggregation on data across grouped rows or an entire table. + /// + /// Syntax: `|> AGGREGATE [[AS] alias], ...` + /// + /// Syntax: + /// ```norust + /// |> AGGREGATE [ [[AS] alias], ...] + /// GROUP BY [AS alias], ... + /// ``` + /// + /// See more at + Aggregate { + full_table_exprs: Vec, + group_by_expr: Vec, + }, + /// Selects a random sample of rows from the input table. + /// Syntax: `|> TABLESAMPLE SYSTEM (10 PERCENT) + /// See more at + TableSample { sample: Box }, + /// Renames columns in the input table. + /// + /// Syntax: `|> RENAME old_name AS new_name, ...` + /// + /// See more at + Rename { mappings: Vec }, + /// Combines the input table with one or more tables using UNION. + /// + /// Syntax: `|> UNION [ALL|DISTINCT] (), (), ...` + /// + /// See more at + Union { + set_quantifier: SetQuantifier, + queries: Vec, + }, + /// Returns only the rows that are present in both the input table and the specified tables. + /// + /// Syntax: `|> INTERSECT [DISTINCT] (), (), ...` + /// + /// See more at + Intersect { + set_quantifier: SetQuantifier, + queries: Vec, + }, + /// Returns only the rows that are present in the input table but not in the specified tables. + /// + /// Syntax: `|> EXCEPT DISTINCT (), (), ...` + /// + /// See more at + Except { + set_quantifier: SetQuantifier, + queries: Vec, + }, + /// Calls a table function or procedure that returns a table. + /// + /// Syntax: `|> CALL function_name(args) [AS alias]` + /// + /// See more at + Call { + function: Function, + alias: Option, + }, + /// Pivots data from rows to columns. + /// + /// Syntax: `|> PIVOT(aggregate_function(column) FOR pivot_column IN (value1, value2, ...)) [AS alias]` + /// + /// See more at + Pivot { + aggregate_functions: Vec, + value_column: Vec, + value_source: PivotValueSource, + alias: Option, + }, + /// The `UNPIVOT` pipe operator transforms columns into rows. + /// + /// Syntax: + /// ```sql + /// |> UNPIVOT(value_column FOR name_column IN (column1, column2, ...)) [alias] + /// ``` + /// + /// See more at + Unpivot { + value_column: Ident, + name_column: Ident, + unpivot_columns: Vec, + alias: Option, + }, + /// Joins the input table with another table. + /// + /// Syntax: `|> [JOIN_TYPE] JOIN
[alias] ON ` or `|> [JOIN_TYPE] JOIN
[alias] USING ()` + /// + /// See more at + Join(Join), +} + +impl fmt::Display for PipeOperator { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PipeOperator::Select { exprs } => { + write!(f, "SELECT {}", display_comma_separated(exprs.as_slice())) + } + PipeOperator::Extend { exprs } => { + write!(f, "EXTEND {}", display_comma_separated(exprs.as_slice())) + } + PipeOperator::Set { assignments } => { + write!(f, "SET {}", display_comma_separated(assignments.as_slice())) + } + PipeOperator::Drop { columns } => { + write!(f, "DROP {}", display_comma_separated(columns.as_slice())) + } + PipeOperator::As { alias } => { + write!(f, "AS {alias}") + } + PipeOperator::Limit { expr, offset } => { + write!(f, "LIMIT {expr}")?; + if let Some(offset) = offset { + write!(f, " OFFSET {offset}")?; + } + Ok(()) + } + PipeOperator::Aggregate { + full_table_exprs, + group_by_expr, + } => { + write!(f, "AGGREGATE")?; + if !full_table_exprs.is_empty() { + write!( + f, + " {}", + display_comma_separated(full_table_exprs.as_slice()) + )?; + } + if !group_by_expr.is_empty() { + write!(f, " GROUP BY {}", display_comma_separated(group_by_expr))?; + } + Ok(()) + } + + PipeOperator::Where { expr } => { + write!(f, "WHERE {expr}") + } + PipeOperator::OrderBy { exprs } => { + write!(f, "ORDER BY {}", display_comma_separated(exprs.as_slice())) + } + + PipeOperator::TableSample { sample } => { + write!(f, "{sample}") + } + PipeOperator::Rename { mappings } => { + write!(f, "RENAME {}", display_comma_separated(mappings)) + } + PipeOperator::Union { + set_quantifier, + queries, + } => Self::fmt_set_operation(f, "UNION", set_quantifier, queries), + PipeOperator::Intersect { + set_quantifier, + queries, + } => Self::fmt_set_operation(f, "INTERSECT", set_quantifier, queries), + PipeOperator::Except { + set_quantifier, + queries, + } => Self::fmt_set_operation(f, "EXCEPT", set_quantifier, queries), + PipeOperator::Call { function, alias } => { + write!(f, "CALL {function}")?; + Self::fmt_optional_alias(f, alias) + } + PipeOperator::Pivot { + aggregate_functions, + value_column, + value_source, + alias, + } => { + write!( + f, + "PIVOT({} FOR {} IN ({}))", + display_comma_separated(aggregate_functions), + Expr::CompoundIdentifier(value_column.to_vec()), + value_source + )?; + Self::fmt_optional_alias(f, alias) + } + PipeOperator::Unpivot { + value_column, + name_column, + unpivot_columns, + alias, + } => { + write!( + f, + "UNPIVOT({} FOR {} IN ({}))", + value_column, + name_column, + display_comma_separated(unpivot_columns) + )?; + Self::fmt_optional_alias(f, alias) + } + PipeOperator::Join(join) => write!(f, "{join}"), + } + } +} + +impl PipeOperator { + /// Helper function to format optional alias for pipe operators + fn fmt_optional_alias(f: &mut fmt::Formatter<'_>, alias: &Option) -> fmt::Result { + if let Some(alias) = alias { + write!(f, " AS {alias}")?; + } + Ok(()) + } + + /// Helper function to format set operations (UNION, INTERSECT, EXCEPT) with queries + fn fmt_set_operation( + f: &mut fmt::Formatter<'_>, + operation: &str, + set_quantifier: &SetQuantifier, + queries: &[Query], + ) -> fmt::Result { + write!(f, "{operation}")?; + match set_quantifier { + SetQuantifier::None => {} + _ => { + write!(f, " {set_quantifier}")?; + } + } + write!(f, " ")?; + let parenthesized_queries: Vec = + queries.iter().map(|query| format!("({query})")).collect(); + write!(f, "{}", display_comma_separated(&parenthesized_queries)) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2666,13 +3125,14 @@ pub struct Values { impl fmt::Display for Values { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "VALUES ")?; + f.write_str("VALUES")?; let prefix = if self.explicit_row { "ROW" } else { "" }; let mut delim = ""; for row in &self.rows { - write!(f, "{delim}")?; - delim = ", "; - write!(f, "{prefix}({})", display_comma_separated(row))?; + f.write_str(delim)?; + delim = ","; + SpaceOrNewline.fmt(f)?; + Indent(format_args!("{prefix}({})", display_comma_separated(row))).fmt(f)?; } Ok(()) } @@ -2759,8 +3219,9 @@ impl fmt::Display for GroupByExpr { Ok(()) } GroupByExpr::Expressions(col_names, modifiers) => { - let col_names = display_comma_separated(col_names); - write!(f, "GROUP BY {col_names}")?; + f.write_str("GROUP BY")?; + SpaceOrNewline.fmt(f)?; + Indent(display_comma_separated(col_names)).fmt(f)?; if !modifiers.is_empty() { write!(f, " {}", display_separated(modifiers, " "))?; } @@ -2784,7 +3245,7 @@ pub enum FormatClause { impl fmt::Display for FormatClause { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - FormatClause::Identifier(ident) => write!(f, "FORMAT {}", ident), + FormatClause::Identifier(ident) => write!(f, "FORMAT {ident}"), FormatClause::Null => write!(f, "FORMAT NULL"), } } @@ -2846,9 +3307,9 @@ impl fmt::Display for ForClause { without_array_wrapper, } => { write!(f, "FOR JSON ")?; - write!(f, "{}", for_json)?; + write!(f, "{for_json}")?; if let Some(root) = root { - write!(f, ", ROOT('{}')", root)?; + write!(f, ", ROOT('{root}')")?; } if *include_null_values { write!(f, ", INCLUDE_NULL_VALUES")?; @@ -2866,7 +3327,7 @@ impl fmt::Display for ForClause { r#type, } => { write!(f, "FOR XML ")?; - write!(f, "{}", for_xml)?; + write!(f, "{for_xml}")?; if *binary_base64 { write!(f, ", BINARY BASE64")?; } @@ -2874,7 +3335,7 @@ impl fmt::Display for ForClause { write!(f, ", TYPE")?; } if let Some(root) = root { - write!(f, ", ROOT('{}')", root)?; + write!(f, ", ROOT('{root}')")?; } if *elements { write!(f, ", ELEMENTS")?; @@ -2901,7 +3362,7 @@ impl fmt::Display for ForXml { ForXml::Raw(root) => { write!(f, "RAW")?; if let Some(root) = root { - write!(f, "('{}')", root)?; + write!(f, "('{root}')")?; } Ok(()) } @@ -2910,7 +3371,7 @@ impl fmt::Display for ForXml { ForXml::Path(root) => { write!(f, "PATH")?; if let Some(root) = root { - write!(f, "('{}')", root)?; + write!(f, "('{root}')")?; } Ok(()) } @@ -2973,7 +3434,7 @@ impl fmt::Display for JsonTableColumn { JsonTableColumn::Named(json_table_named_column) => { write!(f, "{json_table_named_column}") } - JsonTableColumn::ForOrdinality(ident) => write!(f, "{} FOR ORDINALITY", ident), + JsonTableColumn::ForOrdinality(ident) => write!(f, "{ident} FOR ORDINALITY"), JsonTableColumn::Nested(json_table_nested_column) => { write!(f, "{json_table_nested_column}") } @@ -3039,10 +3500,10 @@ impl fmt::Display for JsonTableNamedColumn { self.path )?; if let Some(on_empty) = &self.on_empty { - write!(f, " {} ON EMPTY", on_empty)?; + write!(f, " {on_empty} ON EMPTY")?; } if let Some(on_error) = &self.on_error { - write!(f, " {} ON ERROR", on_error)?; + write!(f, " {on_error} ON ERROR")?; } Ok(()) } @@ -3064,7 +3525,7 @@ impl fmt::Display for JsonTableColumnErrorHandling { match self { JsonTableColumnErrorHandling::Null => write!(f, "NULL"), JsonTableColumnErrorHandling::Default(json_string) => { - write!(f, "DEFAULT {}", json_string) + write!(f, "DEFAULT {json_string}") } JsonTableColumnErrorHandling::Error => write!(f, "ERROR"), } @@ -3106,15 +3567,19 @@ impl fmt::Display for OpenJsonTableColumn { } /// BigQuery supports ValueTables which have 2 modes: -/// `SELECT AS STRUCT` -/// `SELECT AS VALUE` +/// `SELECT [ALL | DISTINCT] AS STRUCT` +/// `SELECT [ALL | DISTINCT] AS VALUE` +/// /// +/// #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ValueTableMode { AsStruct, AsValue, + DistinctAsStruct, + DistinctAsValue, } impl fmt::Display for ValueTableMode { @@ -3122,6 +3587,8 @@ impl fmt::Display for ValueTableMode { match self { ValueTableMode::AsStruct => write!(f, "AS STRUCT"), ValueTableMode::AsValue => write!(f, "AS VALUE"), + ValueTableMode::DistinctAsStruct => write!(f, "DISTINCT AS STRUCT"), + ValueTableMode::DistinctAsValue => write!(f, "DISTINCT AS VALUE"), } } } @@ -3191,12 +3658,12 @@ impl fmt::Display for XmlTableColumn { default, nullable, } => { - write!(f, " {}", r#type)?; + write!(f, " {type}")?; if let Some(p) = path { - write!(f, " PATH {}", p)?; + write!(f, " PATH {p}")?; } if let Some(d) = default { - write!(f, " DEFAULT {}", d)?; + write!(f, " DEFAULT {d}")?; } if !*nullable { write!(f, " NOT NULL")?; @@ -3227,7 +3694,7 @@ impl fmt::Display for XmlPassingArgument { } write!(f, "{}", self.expr)?; if let Some(alias) = &self.alias { - write!(f, " AS {}", alias)?; + write!(f, " AS {alias}")?; } Ok(()) } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 28d479f30..39761751f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -15,7 +15,10 @@ // specific language governing permissions and limitations // under the License. -use crate::ast::query::SelectItemQualifiedWildcardKind; +use crate::ast::{ + ddl::AlterSchema, query::SelectItemQualifiedWildcardKind, AlterSchemaOperation, ColumnOptions, + ExportData, TypedString, +}; use core::iter; use crate::tokenizer::Span; @@ -28,15 +31,16 @@ use super::{ ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, - FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, Insert, Interpolate, - InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, - LimitClause, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, - Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, - PivotValueSource, ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, - ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, - SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, - TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, - TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, + FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, + Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, + LateralView, LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList, + NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, + OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, + ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction, + RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, + SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, + TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, + UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, }; @@ -98,10 +102,11 @@ impl Spanned for Query { order_by, limit_clause, fetch, - locks: _, // todo - for_clause: _, // todo, mssql specific - settings: _, // todo, clickhouse specific - format_clause: _, // todo, clickhouse specific + locks: _, // todo + for_clause: _, // todo, mssql specific + settings: _, // todo, clickhouse specific + format_clause: _, // todo, clickhouse specific + pipe_operators: _, // todo bigquery specific } = self; union_spans( @@ -209,6 +214,7 @@ impl Spanned for SetExpr { SetExpr::Table(_) => Span::empty(), SetExpr::Update(statement) => statement.span(), SetExpr::Delete(statement) => statement.span(), + SetExpr::Merge(statement) => statement.span(), } } } @@ -310,7 +316,6 @@ impl Spanned for Statement { table_names, partitions, table: _, - only: _, identity: _, cascade: _, on_cluster: _, @@ -338,6 +343,7 @@ impl Spanned for Statement { } => source.span(), Statement::Case(stmt) => stmt.span(), Statement::If(stmt) => stmt.span(), + Statement::While(stmt) => stmt.span(), Statement::Raise(stmt) => stmt.span(), Statement::Call(function) => function.span(), Statement::Copy { @@ -364,6 +370,7 @@ impl Spanned for Statement { from_query: _, partition: _, } => Span::empty(), + Statement::Open(open) => open.span(), Statement::Close { cursor } => match cursor { CloseCursor::All => Span::empty(), CloseCursor::Specific { name } => name.span, @@ -375,6 +382,7 @@ impl Spanned for Statement { selection, returning, or: _, + limit: _, } => union_spans( core::iter::once(table.span()) .chain(assignments.iter().map(|i| i.span())) @@ -397,6 +405,7 @@ impl Spanned for Statement { if_not_exists: _, temporary: _, to, + name_before_not_exists: _, params: _, } => union_spans( core::iter::once(name.span()) @@ -420,6 +429,7 @@ impl Spanned for Statement { Statement::CreateIndex(create_index) => create_index.span(), Statement::CreateRole { .. } => Span::empty(), Statement::CreateSecret { .. } => Span::empty(), + Statement::CreateServer { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), Statement::AlterTable { name, @@ -428,10 +438,13 @@ impl Spanned for Statement { operations, location: _, on_cluster, + iceberg: _, + end_token, } => union_spans( core::iter::once(name.span()) .chain(operations.iter().map(|i| i.span())) - .chain(on_cluster.iter().map(|i| i.span)), + .chain(on_cluster.iter().map(|i| i.span)) + .chain(core::iter::once(end_token.0.span)), ), Statement::AlterIndex { name, operation } => name.span().union(&operation.span()), Statement::AlterView { @@ -454,6 +467,7 @@ impl Spanned for Statement { Statement::DetachDuckDBDatabase { .. } => Span::empty(), Statement::Drop { .. } => Span::empty(), Statement::DropFunction { .. } => Span::empty(), + Statement::DropDomain { .. } => Span::empty(), Statement::DropProcedure { .. } => Span::empty(), Statement::DropSecret { .. } => Span::empty(), Statement::Declare { .. } => Span::empty(), @@ -471,6 +485,7 @@ impl Spanned for Statement { Statement::ShowColumns { .. } => Span::empty(), Statement::ShowTables { .. } => Span::empty(), Statement::ShowCollation { .. } => Span::empty(), + Statement::ShowCharset { .. } => Span::empty(), Statement::Use(u) => u.span(), Statement::StartTransaction { .. } => Span::empty(), Statement::Comment { .. } => Span::empty(), @@ -479,6 +494,7 @@ impl Spanned for Statement { Statement::CreateSchema { .. } => Span::empty(), Statement::CreateDatabase { .. } => Span::empty(), Statement::CreateFunction { .. } => Span::empty(), + Statement::CreateDomain { .. } => Span::empty(), Statement::CreateTrigger { .. } => Span::empty(), Statement::DropTrigger { .. } => Span::empty(), Statement::CreateProcedure { .. } => Span::empty(), @@ -486,6 +502,7 @@ impl Spanned for Statement { Statement::CreateStage { .. } => Span::empty(), Statement::Assert { .. } => Span::empty(), Statement::Grant { .. } => Span::empty(), + Statement::Deny { .. } => Span::empty(), Statement::Revoke { .. } => Span::empty(), Statement::Deallocate { .. } => Span::empty(), Statement::Execute { .. } => Span::empty(), @@ -523,6 +540,20 @@ impl Spanned for Statement { Statement::Print { .. } => Span::empty(), Statement::Return { .. } => Span::empty(), Statement::List(..) | Statement::Remove(..) => Span::empty(), + Statement::ExportData(ExportData { + options, + query, + connection, + }) => union_spans( + options + .iter() + .map(|i| i.span()) + .chain(core::iter::once(query.span())) + .chain(connection.iter().map(|i| i.span())), + ), + Statement::CreateUser(..) => Span::empty(), + Statement::AlterSchema(s) => s.span(), + Statement::Vacuum(..) => Span::empty(), } } } @@ -554,6 +585,7 @@ impl Spanned for CreateTable { temporary: _, // bool external: _, // bool global: _, // bool + dynamic: _, // bool if_not_exists: _, // bool transient: _, // bool volatile: _, // bool @@ -563,27 +595,20 @@ impl Spanned for CreateTable { constraints, hive_distribution: _, // hive specific hive_formats: _, // hive specific - table_properties, - with_options, - file_format: _, // enum - location: _, // string, no span + file_format: _, // enum + location: _, // string, no span query, without_rowid: _, // bool - like, + like: _, clone, - engine: _, // todo - comment: _, // todo, no span - auto_increment_offset: _, // u32, no span - default_charset: _, // string, no span - collation: _, // string, no span - on_commit: _, // enum + comment: _, // todo, no span + on_commit: _, on_cluster: _, // todo, clickhouse specific primary_key: _, // todo, clickhouse specific order_by: _, // todo, clickhouse specific partition_by: _, // todo, BigQuery specific cluster_by: _, // todo, BigQuery specific clustered_by: _, // todo, Hive specific - options: _, // todo, BigQuery specific inherits: _, // todo, PostgreSQL specific strict: _, // bool copy_grants: _, // bool @@ -599,17 +624,22 @@ impl Spanned for CreateTable { base_location: _, // todo, Snowflake specific catalog: _, // todo, Snowflake specific catalog_sync: _, // todo, Snowflake specific - storage_serialization_policy: _, // todo, Snowflake specific + storage_serialization_policy: _, + table_options, + target_lag: _, + warehouse: _, + version: _, + refresh_mode: _, + initialize: _, + require_user: _, } = self; union_spans( core::iter::once(name.span()) + .chain(core::iter::once(table_options.span())) .chain(columns.iter().map(|i| i.span())) .chain(constraints.iter().map(|i| i.span())) - .chain(table_properties.iter().map(|i| i.span())) - .chain(with_options.iter().map(|i| i.span())) .chain(query.iter().map(|i| i.span())) - .chain(like.iter().map(|i| i.span())) .chain(clone.iter().map(|i| i.span())), ) } @@ -651,7 +681,7 @@ impl Spanned for TableConstraint { name.iter() .map(|i| i.span) .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(columns.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), TableConstraint::PrimaryKey { @@ -665,12 +695,13 @@ impl Spanned for TableConstraint { name.iter() .map(|i| i.span) .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(columns.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), TableConstraint::ForeignKey { name, columns, + index_name, foreign_table, referred_columns, on_delete, @@ -679,6 +710,7 @@ impl Spanned for TableConstraint { } => union_spans( name.iter() .map(|i| i.span) + .chain(index_name.iter().map(|i| i.span)) .chain(columns.iter().map(|i| i.span)) .chain(core::iter::once(foreign_table.span())) .chain(referred_columns.iter().map(|i| i.span)) @@ -686,18 +718,21 @@ impl Spanned for TableConstraint { .chain(on_update.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), - TableConstraint::Check { name, expr } => { - expr.span().union_opt(&name.as_ref().map(|i| i.span)) - } + TableConstraint::Check { + name, + expr, + enforced: _, + } => expr.span().union_opt(&name.as_ref().map(|i| i.span)), TableConstraint::Index { display_as_key: _, name, index_type: _, columns, + index_options: _, } => union_spans( name.iter() .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), + .chain(columns.iter().map(|i| i.span())), ), TableConstraint::FulltextOrSpatial { fulltext: _, @@ -708,7 +743,7 @@ impl Spanned for TableConstraint { opt_index_name .iter() .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), + .chain(columns.iter().map(|i| i.span())), ), } } @@ -728,6 +763,8 @@ impl Spanned for CreateIndex { nulls_distinct: _, // bool with, predicate, + index_options: _, + alter_options, } = self; union_spans( @@ -737,11 +774,18 @@ impl Spanned for CreateIndex { .chain(columns.iter().map(|i| i.column.span())) .chain(include.iter().map(|i| i.span)) .chain(with.iter().map(|i| i.span())) - .chain(predicate.iter().map(|i| i.span())), + .chain(predicate.iter().map(|i| i.span())) + .chain(alter_options.iter().map(|i| i.span())), ) } } +impl Spanned for IndexColumn { + fn span(&self) -> Span { + self.column.span() + } +} + impl Spanned for CaseStatement { fn span(&self) -> Span { let CaseStatement { @@ -774,6 +818,14 @@ impl Spanned for IfStatement { } } +impl Spanned for WhileStatement { + fn span(&self) -> Span { + let WhileStatement { while_block } = self; + + while_block.span() + } +} + impl Spanned for ConditionalStatements { fn span(&self) -> Span { match self { @@ -864,6 +916,7 @@ impl Spanned for ColumnOption { ColumnOption::OnConflict(..) => Span::empty(), ColumnOption::Policy(..) => Span::empty(), ColumnOption::Tags(..) => Span::empty(), + ColumnOption::Srid(..) => Span::empty(), } } } @@ -905,6 +958,7 @@ impl Spanned for AlterColumnOperation { AlterColumnOperation::SetDataType { data_type: _, using, + had_set: _, } => using.as_ref().map_or(Span::empty(), |u| u.span()), AlterColumnOperation::AddGenerated { .. } => Span::empty(), } @@ -972,10 +1026,13 @@ impl Spanned for ViewColumnDef { options, } = self; - union_spans( - core::iter::once(name.span) - .chain(options.iter().flat_map(|i| i.iter().map(|k| k.span()))), - ) + name.span.union_opt(&options.as_ref().map(|o| o.span())) + } +} + +impl Spanned for ColumnOptions { + fn span(&self) -> Span { + union_spans(self.as_slice().iter().map(|i| i.span())) } } @@ -992,6 +1049,14 @@ impl Spanned for SqlOption { } => union_spans( core::iter::once(column_name.span).chain(for_values.iter().map(|i| i.span())), ), + SqlOption::TableSpace(_) => Span::empty(), + SqlOption::Comment(_) => Span::empty(), + SqlOption::NamedParenthesizedList(NamedParenthesizedList { + key: name, + name: value, + values, + }) => union_spans(core::iter::once(name.span).chain(values.iter().map(|i| i.span))) + .union_opt(&value.as_ref().map(|i| i.span)), } } } @@ -1028,7 +1093,11 @@ impl Spanned for CreateTableOptions { match self { CreateTableOptions::None => Span::empty(), CreateTableOptions::With(vec) => union_spans(vec.iter().map(|i| i.span())), - CreateTableOptions::Options(vec) => union_spans(vec.iter().map(|i| i.span())), + CreateTableOptions::Options(vec) => { + union_spans(vec.as_slice().iter().map(|i| i.span())) + } + CreateTableOptions::Plain(vec) => union_spans(vec.iter().map(|i| i.span())), + CreateTableOptions::TableProperties(vec) => union_spans(vec.iter().map(|i| i.span())), } } } @@ -1040,7 +1109,10 @@ impl Spanned for CreateTableOptions { impl Spanned for AlterTableOperation { fn span(&self) -> Span { match self { - AlterTableOperation::AddConstraint(table_constraint) => table_constraint.span(), + AlterTableOperation::AddConstraint { + constraint, + not_valid: _, + } => constraint.span(), AlterTableOperation::AddColumn { column_keyword: _, if_not_exists: _, @@ -1072,10 +1144,11 @@ impl Spanned for AlterTableOperation { drop_behavior: _, } => name.span, AlterTableOperation::DropColumn { - column_name, + has_column_keyword: _, + column_names, if_exists: _, drop_behavior: _, - } => column_name.span, + } => union_spans(column_names.iter().map(|i| i.span)), AlterTableOperation::AttachPartition { partition } => partition.span(), AlterTableOperation::DetachPartition { partition } => partition.span(), AlterTableOperation::FreezePartition { @@ -1090,8 +1163,9 @@ impl Spanned for AlterTableOperation { } => partition .span() .union_opt(&with_name.as_ref().map(|n| n.span)), - AlterTableOperation::DropPrimaryKey => Span::empty(), - AlterTableOperation::DropForeignKey { name } => name.span, + AlterTableOperation::DropPrimaryKey { .. } => Span::empty(), + AlterTableOperation::DropForeignKey { name, .. } => name.span, + AlterTableOperation::DropIndex { name } => name.span, AlterTableOperation::EnableAlwaysRule { name } => name.span, AlterTableOperation::EnableAlwaysTrigger { name } => name.span, AlterTableOperation::EnableReplicaRule { name } => name.span, @@ -1158,6 +1232,11 @@ impl Spanned for AlterTableOperation { AlterTableOperation::Algorithm { .. } => Span::empty(), AlterTableOperation::AutoIncrement { value, .. } => value.span(), AlterTableOperation::Lock { .. } => Span::empty(), + AlterTableOperation::ReplicaIdentity { .. } => Span::empty(), + AlterTableOperation::ValidateConstraint { name } => name.span, + AlterTableOperation::SetOptionsParens { options } => { + union_spans(options.iter().map(|i| i.span())) + } } } } @@ -1373,7 +1452,6 @@ impl Spanned for AssignmentTarget { /// f.e. `IS NULL ` reports as `::span`. /// /// Missing spans: -/// - [Expr::TypedString] # missing span for data_type /// - [Expr::MatchAgainst] # MySQL specific /// - [Expr::RLike] # MySQL specific /// - [Expr::Struct] # BigQuery specific @@ -1468,7 +1546,7 @@ impl Spanned for Expr { .union(&union_spans(collation.0.iter().map(|i| i.span()))), Expr::Nested(expr) => expr.span(), Expr::Value(value) => value.span(), - Expr::TypedString { value, .. } => value.span(), + Expr::TypedString(TypedString { value, .. }) => value.span(), Expr::Function(function) => function.span(), Expr::GroupingSets(vec) => { union_spans(vec.iter().flat_map(|i| i.iter().map(|k| k.span()))) @@ -1545,18 +1623,24 @@ impl Spanned for Expr { ), Expr::Prefixed { value, .. } => value.span(), Expr::Case { + case_token, + end_token, operand, conditions, else_result, } => union_spans( - operand - .as_ref() - .map(|i| i.span()) - .into_iter() - .chain(conditions.iter().flat_map(|case_when| { - [case_when.condition.span(), case_when.result.span()] - })) - .chain(else_result.as_ref().map(|i| i.span())), + iter::once(case_token.0.span) + .chain( + operand + .as_ref() + .map(|i| i.span()) + .into_iter() + .chain(conditions.iter().flat_map(|case_when| { + [case_when.condition.span(), case_when.result.span()] + })) + .chain(else_result.as_ref().map(|i| i.span())), + ) + .chain(iter::once(end_token.0.span)), ), Expr::Exists { subquery, .. } => subquery.span(), Expr::Subquery(query) => query.span(), @@ -1576,6 +1660,7 @@ impl Spanned for Expr { Expr::OuterJoin(expr) => expr.span(), Expr::Prior(expr) => expr.span(), Expr::Lambda(_) => Span::empty(), + Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()), } } } @@ -1622,6 +1707,10 @@ impl Spanned for ObjectNamePart { fn span(&self) -> Span { match self { ObjectNamePart::Identifier(ident) => ident.span, + ObjectNamePart::Function(func) => func + .name + .span + .union(&union_spans(func.args.iter().map(|i| i.span()))), } } } @@ -1918,7 +2007,7 @@ impl Spanned for TableFactor { } => union_spans( core::iter::once(table.span()) .chain(aggregate_functions.iter().map(|i| i.span())) - .chain(value_column.iter().map(|i| i.span)) + .chain(value_column.iter().map(|i| i.span())) .chain(core::iter::once(value_source.span())) .chain(default_on_null.as_ref().map(|i| i.span())) .chain(alias.as_ref().map(|i| i.span())), @@ -1926,14 +2015,15 @@ impl Spanned for TableFactor { TableFactor::Unpivot { table, value, + null_inclusion: _, name, columns, alias, } => union_spans( core::iter::once(table.span()) - .chain(core::iter::once(value.span)) + .chain(core::iter::once(value.span())) .chain(core::iter::once(name.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(columns.iter().map(|ilist| ilist.span())) .chain(alias.as_ref().map(|alias| alias.span())), ), TableFactor::MatchRecognize { @@ -1955,6 +2045,23 @@ impl Spanned for TableFactor { .chain(symbols.iter().map(|i| i.span())) .chain(alias.as_ref().map(|i| i.span())), ), + TableFactor::SemanticView { + name, + dimensions, + metrics, + facts, + where_clause, + alias, + } => union_spans( + name.0 + .iter() + .map(|i| i.span()) + .chain(dimensions.iter().map(|d| d.span())) + .chain(metrics.iter().map(|m| m.span())) + .chain(facts.iter().map(|f| f.span())) + .chain(where_clause.as_ref().map(|e| e.span())) + .chain(alias.as_ref().map(|a| a.span())), + ), TableFactor::OpenJsonTable { .. } => Span::empty(), } } @@ -2166,6 +2273,7 @@ impl Spanned for Select { distinct: _, // todo top: _, // todo, mysql specific projection, + exclude: _, into, from, lateral_views, @@ -2295,6 +2403,37 @@ impl Spanned for BeginEndStatements { } } +impl Spanned for OpenStatement { + fn span(&self) -> Span { + let OpenStatement { cursor_name } = self; + cursor_name.span + } +} + +impl Spanned for AlterSchemaOperation { + fn span(&self) -> Span { + match self { + AlterSchemaOperation::SetDefaultCollate { collate } => collate.span(), + AlterSchemaOperation::AddReplica { replica, options } => union_spans( + core::iter::once(replica.span) + .chain(options.iter().flat_map(|i| i.iter().map(|i| i.span()))), + ), + AlterSchemaOperation::DropReplica { replica } => replica.span, + AlterSchemaOperation::SetOptionsParens { options } => { + union_spans(options.iter().map(|i| i.span())) + } + } + } +} + +impl Spanned for AlterSchema { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()).chain(self.operations.iter().map(|i| i.span())), + ) + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; @@ -2434,4 +2573,55 @@ pub mod tests { assert_eq!(test.get_source(body_span), "SELECT cte.* FROM cte"); } + + #[test] + fn test_case_expr_span() { + let dialect = &GenericDialect; + let mut test = SpanTest::new(dialect, "CASE 1 WHEN 2 THEN 3 ELSE 4 END"); + let expr = test.0.parse_expr().unwrap(); + let expr_span = expr.span(); + assert_eq!( + test.get_source(expr_span), + "CASE 1 WHEN 2 THEN 3 ELSE 4 END" + ); + } + + #[test] + fn test_placeholder_span() { + let sql = "\nSELECT\n :fooBar"; + let r = Parser::parse_sql(&GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + match &r[0] { + Statement::Query(q) => { + let col = &q.body.as_select().unwrap().projection[0]; + match col { + SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan { + value: Value::Placeholder(s), + span, + })) => { + assert_eq!(":fooBar", s); + assert_eq!(&Span::new((3, 3).into(), (3, 10).into()), span); + } + _ => panic!("expected unnamed expression; got {col:?}"), + } + } + stmt => panic!("expected query; got {stmt:?}"), + } + } + + #[test] + fn test_alter_table_multiline_span() { + let sql = r#"-- foo +ALTER TABLE users + ADD COLUMN foo + varchar; -- hi there"#; + + let r = Parser::parse_sql(&crate::dialect::PostgreSqlDialect {}, sql).unwrap(); + assert_eq!(1, r.len()); + + let stmt_span = r[0].span(); + + assert_eq!(stmt_span.start, (2, 13).into()); + assert_eq!(stmt_span.end, (4, 11).into()); + } } diff --git a/src/ast/trigger.rs b/src/ast/trigger.rs index cf1c8c466..2c64e4239 100644 --- a/src/ast/trigger.rs +++ b/src/ast/trigger.rs @@ -110,6 +110,7 @@ impl fmt::Display for TriggerEvent { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TriggerPeriod { + For, After, Before, InsteadOf, @@ -118,6 +119,7 @@ pub enum TriggerPeriod { impl fmt::Display for TriggerPeriod { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + TriggerPeriod::For => write!(f, "FOR"), TriggerPeriod::After => write!(f, "AFTER"), TriggerPeriod::Before => write!(f, "BEFORE"), TriggerPeriod::InsteadOf => write!(f, "INSTEAD OF"), diff --git a/src/ast/value.rs b/src/ast/value.rs index 77e2e0e81..fdfa6a674 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -116,7 +116,6 @@ impl From for Value { derive(Visit, VisitMut), visit(with = "visit_value") )] - pub enum Value { /// Numeric literal #[cfg(not(feature = "bigdecimal"))] @@ -456,30 +455,38 @@ impl fmt::Display for EscapeQuotedString<'_> { // | `"A\"B\"A"` | default | `DoubleQuotedString(String::from("A\"B\"A"))` | `"A""B""A"` | let quote = self.quote; let mut previous_char = char::default(); - let mut peekable_chars = self.string.chars().peekable(); - while let Some(&ch) = peekable_chars.peek() { + let mut start_idx = 0; + let mut peekable_chars = self.string.char_indices().peekable(); + while let Some(&(idx, ch)) = peekable_chars.peek() { match ch { char if char == quote => { if previous_char == '\\' { - write!(f, "{char}")?; + // the quote is already escaped with a backslash, skip peekable_chars.next(); continue; } peekable_chars.next(); - if peekable_chars.peek().map(|c| *c == quote).unwrap_or(false) { - write!(f, "{char}{char}")?; - peekable_chars.next(); - } else { - write!(f, "{char}{char}")?; + match peekable_chars.peek() { + Some((_, c)) if *c == quote => { + // the quote is already escaped with another quote, skip + peekable_chars.next(); + } + _ => { + // The quote is not escaped. + // Including idx in the range, so the quote at idx will be printed twice: + // in this call to write_str() and in the next one. + f.write_str(&self.string[start_idx..=idx])?; + start_idx = idx; + } } } _ => { - write!(f, "{ch}")?; peekable_chars.next(); } } previous_char = ch; } + f.write_str(&self.string[start_idx..])?; Ok(()) } } @@ -543,16 +550,16 @@ impl fmt::Display for EscapeUnicodeStringLiteral<'_> { write!(f, r#"\\"#)?; } x if x.is_ascii() => { - write!(f, "{}", c)?; + write!(f, "{c}")?; } _ => { let codepoint = c as u32; // if the character fits in 32 bits, we can use the \XXXX format // otherwise, we need to use the \+XXXXXX format if codepoint <= 0xFFFF { - write!(f, "\\{:04X}", codepoint)?; + write!(f, "\\{codepoint:04X}")?; } else { - write!(f, "\\+{:06X}", codepoint)?; + write!(f, "\\+{codepoint:06X}")?; } } } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 50985a3e7..7840f0e14 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -741,7 +741,7 @@ mod tests { } } - fn do_visit(sql: &str, visitor: &mut V) -> Statement { + fn do_visit>(sql: &str, visitor: &mut V) -> Statement { let dialect = GenericDialect {}; let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let s = Parser::new(&dialect) @@ -749,7 +749,8 @@ mod tests { .parse_statement() .unwrap(); - s.visit(visitor); + let flow = s.visit(visitor); + assert_eq!(flow, ControlFlow::Continue(())); s } @@ -883,6 +884,8 @@ mod tests { "PRE: EXPR: a.amount", "POST: EXPR: a.amount", "POST: EXPR: SUM(a.amount)", + "PRE: EXPR: a.MONTH", + "POST: EXPR: a.MONTH", "PRE: EXPR: 'JAN'", "POST: EXPR: 'JAN'", "PRE: EXPR: 'FEB'", @@ -925,10 +928,10 @@ mod tests { #[test] fn overflow() { let cond = (0..1000) - .map(|n| format!("X = {}", n)) + .map(|n| format!("X = {n}")) .collect::>() .join(" OR "); - let sql = format!("SELECT x where {0}", cond); + let sql = format!("SELECT x where {cond}"); let dialect = GenericDialect {}; let tokens = Tokenizer::new(&dialect, sql.as_str()).tokenize().unwrap(); @@ -938,7 +941,8 @@ mod tests { .unwrap(); let mut visitor = QuickVisitor {}; - s.visit(&mut visitor); + let flow = s.visit(&mut visitor); + assert_eq!(flow, ControlFlow::Continue(())); } } @@ -969,7 +973,7 @@ mod visit_mut_tests { } } - fn do_visit_mut(sql: &str, visitor: &mut V) -> Statement { + fn do_visit_mut>(sql: &str, visitor: &mut V) -> Statement { let dialect = GenericDialect {}; let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let mut s = Parser::new(&dialect) @@ -977,7 +981,8 @@ mod visit_mut_tests { .parse_statement() .unwrap(); - s.visit(visitor); + let flow = s.visit(visitor); + assert_eq!(flow, ControlFlow::Continue(())); s } diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 49fb24f19..27fd3cca3 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -19,6 +19,7 @@ use crate::ast::Statement; use crate::dialect::Dialect; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; +use crate::tokenizer::Token; /// These keywords are disallowed as column identifiers. Such that /// `SELECT 5 AS FROM T` is rejected by BigQuery. @@ -46,7 +47,18 @@ pub struct BigQueryDialect; impl Dialect for BigQueryDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { - self.maybe_parse_statement(parser) + if parser.parse_keyword(Keyword::BEGIN) { + if parser.peek_keyword(Keyword::TRANSACTION) + || parser.peek_token_ref().token == Token::SemiColon + || parser.peek_token_ref().token == Token::EOF + { + parser.prev_token(); + return None; + } + return Some(parser.parse_begin_exception_end()); + } + + None } /// See @@ -136,49 +148,12 @@ impl Dialect for BigQueryDialect { fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { !RESERVED_FOR_COLUMN_ALIAS.contains(kw) } -} -impl BigQueryDialect { - fn maybe_parse_statement(&self, parser: &mut Parser) -> Option> { - if parser.peek_keyword(Keyword::BEGIN) { - return Some(self.parse_begin(parser)); - } - None + fn supports_pipe_operator(&self) -> bool { + true } - /// Parse a `BEGIN` statement. - /// - fn parse_begin(&self, parser: &mut Parser) -> Result { - parser.expect_keyword(Keyword::BEGIN)?; - - let statements = parser.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?; - - let has_exception_when_clause = parser.parse_keywords(&[ - Keyword::EXCEPTION, - Keyword::WHEN, - Keyword::ERROR, - Keyword::THEN, - ]); - let exception_statements = if has_exception_when_clause { - if !parser.peek_keyword(Keyword::END) { - Some(parser.parse_statement_list(&[Keyword::END])?) - } else { - Some(Default::default()) - } - } else { - None - }; - - parser.expect_keyword(Keyword::END)?; - - Ok(Statement::StartTransaction { - begin: true, - statements, - exception_statements, - has_end_keyword: true, - transaction: None, - modifier: None, - modes: Default::default(), - }) + fn supports_create_table_multi_schema_info_sources(&self) -> bool { + true } } diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 3366c6705..37cf7d369 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -65,7 +65,7 @@ impl Dialect for DuckDbDialect { true } - /// See + /// See fn supports_lambda_functions(&self) -> bool { true } @@ -94,4 +94,14 @@ impl Dialect for DuckDbDialect { fn supports_order_by_all(&self) -> bool { true } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } + + /// DuckDB supports `NOTNULL` as an alias for `IS NOT NULL`, + /// see DuckDB Comparisons + fn supports_notnull_operator(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 8f57e487f..dffc5b527 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -52,6 +52,10 @@ impl Dialect for GenericDialect { true } + fn supports_left_associative_joins_without_parens(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } @@ -60,6 +64,10 @@ impl Dialect for GenericDialect { true } + fn supports_pipe_operator(&self) -> bool { + true + } + fn supports_start_transaction_modifier(&self) -> bool { true } @@ -108,6 +116,14 @@ impl Dialect for GenericDialect { true } + fn supports_from_first_select(&self) -> bool { + true + } + + fn supports_projection_trailing_commas(&self) -> bool { + true + } + fn supports_asc_desc_in_column_definition(&self) -> bool { true } @@ -167,4 +183,16 @@ impl Dialect for GenericDialect { fn supports_filter_during_aggregation(&self) -> bool { true } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } + + fn supports_data_type_signed_suffix(&self) -> bool { + true + } + + fn supports_interval_options(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b2dff065e..f91209722 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -49,7 +49,7 @@ pub use self::postgresql::PostgreSqlDialect; pub use self::redshift::RedshiftSqlDialect; pub use self::snowflake::SnowflakeDialect; pub use self::sqlite::SQLiteDialect; -use crate::ast::{ColumnOption, Expr, Statement}; +use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement}; pub use crate::keywords; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -278,6 +278,34 @@ pub trait Dialect: Debug + Any { false } + /// Indicates whether the dialect supports left-associative join parsing + /// by default when parentheses are omitted in nested joins. + /// + /// Most dialects (like MySQL or Postgres) assume **left-associative** precedence, + /// so a query like: + /// + /// ```sql + /// SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON ... + /// ``` + /// is interpreted as: + /// ```sql + /// ((t1 NATURAL JOIN t5) INNER JOIN t0 ON ...) + /// ``` + /// and internally represented as a **flat list** of joins. + /// + /// In contrast, some dialects (e.g. **Snowflake**) assume **right-associative** + /// precedence and interpret the same query as: + /// ```sql + /// (t1 NATURAL JOIN (t5 INNER JOIN t0 ON ...)) + /// ``` + /// which results in a **nested join** structure in the AST. + /// + /// If this method returns `false`, the parser must build nested join trees + /// even in the absence of parentheses to reflect the correct associativity + fn supports_left_associative_joins_without_parens(&self) -> bool { + true + } + /// Returns true if the dialect supports the `(+)` syntax for OUTER JOIN. fn supports_outer_join_operator(&self) -> bool { false @@ -518,6 +546,20 @@ pub trait Dialect: Debug + Any { false } + /// Return true if the dialect supports pipe operator. + /// + /// Example: + /// ```sql + /// SELECT * + /// FROM table + /// |> limit 1 + /// ``` + /// + /// See + fn supports_pipe_operator(&self) -> bool { + false + } + /// Does the dialect support MySQL-style `'user'@'host'` grantee syntax? fn supports_user_host_grantee(&self) -> bool { false @@ -528,6 +570,33 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports an exclude option + /// following a wildcard in the projection section. For example: + /// `SELECT * EXCLUDE col1 FROM tbl`. + /// + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html) + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/select) + fn supports_select_wildcard_exclude(&self) -> bool { + false + } + + /// Returns true if the dialect supports an exclude option + /// as the last item in the projection section, not necessarily + /// after a wildcard. For example: + /// `SELECT *, c1, c2 EXCLUDE c3 FROM tbl` + /// + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html) + fn supports_select_exclude(&self) -> bool { + false + } + + /// Return true if the dialect supports specifying multiple options + /// in a `CREATE TABLE` statement for the structure of the new table. For example: + /// `CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a` + fn supports_create_table_multi_schema_info_sources(&self) -> bool { + false + } + /// Dialect-specific infix parser override /// /// This method is called to parse the next infix expression. @@ -573,7 +642,7 @@ pub trait Dialect: Debug + Any { } let token = parser.peek_token(); - debug!("get_next_precedence_full() {:?}", token); + debug!("get_next_precedence_full() {token:?}"); match token.token { Token::Word(w) if w.keyword == Keyword::OR => Ok(p!(Or)), Token::Word(w) if w.keyword == Keyword::AND => Ok(p!(And)), @@ -605,9 +674,19 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)), + Token::Word(w) + if w.keyword == Keyword::NULL && !parser.in_column_definition_state() => + { + Ok(p!(Is)) + } _ => Ok(self.prec_unknown()), }, + Token::Word(w) if w.keyword == Keyword::NOTNULL && self.supports_notnull_operator() => { + Ok(p!(Is)) + } Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)), Token::Word(w) if w.keyword == Keyword::IN => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(p!(Between)), @@ -616,7 +695,9 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)), Token::Period => Ok(p!(Period)), @@ -882,18 +963,17 @@ pub trait Dialect: Debug + Any { keywords::RESERVED_FOR_IDENTIFIER.contains(&kw) } - /// Returns reserved keywords when looking to parse a `TableFactor`. - /// See [Self::supports_from_trailing_commas] - fn get_reserved_keywords_for_table_factor(&self) -> &[Keyword] { - keywords::RESERVED_FOR_TABLE_FACTOR - } - /// Returns reserved keywords that may prefix a select item expression /// e.g. `SELECT CONNECT_BY_ROOT name FROM Tbl2` (Snowflake) fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] { &[] } + /// Returns grantee types that should be treated as identifiers + fn get_reserved_grantees_types(&self) -> &[GranteesType] { + &[] + } + /// Returns true if this dialect supports the `TABLESAMPLE` option /// before the table alias option. For example: /// @@ -941,11 +1021,23 @@ pub trait Dialect: Debug + Any { explicit || self.is_column_alias(kw, parser) } + /// Returns true if the specified keyword should be parsed as a table factor identifier. + /// See [keywords::RESERVED_FOR_TABLE_FACTOR] + fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + !keywords::RESERVED_FOR_TABLE_FACTOR.contains(kw) + } + + /// Returns true if the specified keyword should be parsed as a table factor alias. + /// See [keywords::RESERVED_FOR_TABLE_ALIAS] + fn is_table_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) + } + /// Returns true if the specified keyword should be parsed as a table factor alias. /// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided /// to enable looking ahead if needed. - fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { - explicit || !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) + fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + explicit || self.is_table_alias(kw, parser) } /// Returns true if this dialect supports querying historical table data @@ -1007,6 +1099,103 @@ pub trait Dialect: Debug + Any { fn supports_set_names(&self) -> bool { false } + + fn supports_space_separated_column_options(&self) -> bool { + false + } + + /// Returns true if the dialect supports the `USING` clause in an `ALTER COLUMN` statement. + /// Example: + /// ```sql + /// ALTER TABLE tbl ALTER COLUMN col SET DATA TYPE USING ` + /// ``` + fn supports_alter_column_type_using(&self) -> bool { + false + } + + /// Returns true if the dialect supports `ALTER TABLE tbl DROP COLUMN c1, ..., cn` + fn supports_comma_separated_drop_column_list(&self) -> bool { + false + } + + /// Returns true if the dialect considers the specified ident as a function + /// that returns an identifier. Typically used to generate identifiers + /// programmatically. + /// + /// - [Snowflake](https://docs.snowflake.com/en/sql-reference/identifier-literal) + fn is_identifier_generating_function_name( + &self, + _ident: &Ident, + _name_parts: &[ObjectNamePart], + ) -> bool { + false + } + + /// Returns true if the dialect supports the `x NOTNULL` + /// operator expression. + fn supports_notnull_operator(&self) -> bool { + false + } + + /// Returns true if this dialect allows an optional `SIGNED` suffix after integer data types. + /// + /// Example: + /// ```sql + /// CREATE TABLE t (i INT(20) SIGNED); + /// ``` + /// + /// Note that this is canonicalized to `INT(20)`. + fn supports_data_type_signed_suffix(&self) -> bool { + false + } + + /// Returns true if the dialect supports the `INTERVAL` data type with [Postgres]-style options. + /// + /// Examples: + /// ```sql + /// CREATE TABLE t (i INTERVAL YEAR TO MONTH); + /// SELECT '1 second'::INTERVAL HOUR TO SECOND(3); + /// ``` + /// + /// See [`crate::ast::DataType::Interval`] and [`crate::ast::IntervalFields`]. + /// + /// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html + fn supports_interval_options(&self) -> bool { + false + } + + /// Returns true if the dialect supports specifying which table to copy + /// the schema from inside parenthesis. + /// + /// Not parenthesized: + /// '''sql + /// CREATE TABLE new LIKE old ... + /// ''' + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-table#label-create-table-like) + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_table_like) + /// + /// Parenthesized: + /// '''sql + /// CREATE TABLE new (LIKE old ...) + /// ''' + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_NEW.html) + fn supports_create_table_like_parenthesized(&self) -> bool { + false + } + + /// Returns true if the dialect supports `SEMANTIC_VIEW()` table functions. + /// + /// ```sql + /// SELECT * FROM SEMANTIC_VIEW( + /// model_name + /// DIMENSIONS customer.name, customer.region + /// METRICS orders.revenue, orders.count + /// WHERE customer.active = true + /// ) + /// ``` + fn supports_semantic_view_table_factor(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 31e324f06..518dab248 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -17,7 +17,8 @@ use crate::ast::helpers::attached_token::AttachedToken; use crate::ast::{ - BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, IfStatement, Statement, + BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, GranteesType, + IfStatement, Statement, TriggerObject, }; use crate::dialect::Dialect; use crate::keywords::{self, Keyword}; @@ -51,6 +52,10 @@ impl Dialect for MsSqlDialect { || ch == '_' } + fn identifier_quote_style(&self, _identifier: &str) -> Option { + Some('[') + } + /// SQL Server has `CONVERT(type, value)` instead of `CONVERT(value, type)` /// fn convert_type_before_value(&self) -> bool { @@ -118,6 +123,11 @@ impl Dialect for MsSqlDialect { true } + /// See + fn get_reserved_grantees_types(&self) -> &[GranteesType] { + &[GranteesType::Public] + } + fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw) && !RESERVED_FOR_COLUMN_ALIAS.contains(kw) } @@ -125,6 +135,15 @@ impl Dialect for MsSqlDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.peek_keyword(Keyword::IF) { Some(self.parse_if_stmt(parser)) + } else if parser.parse_keywords(&[Keyword::CREATE, Keyword::TRIGGER]) { + Some(self.parse_create_trigger(parser, false)) + } else if parser.parse_keywords(&[ + Keyword::CREATE, + Keyword::OR, + Keyword::ALTER, + Keyword::TRIGGER, + ]) { + Some(self.parse_create_trigger(parser, true)) } else { None } @@ -215,6 +234,44 @@ impl MsSqlDialect { })) } + /// Parse `CREATE TRIGGER` for [MsSql] + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql + fn parse_create_trigger( + &self, + parser: &mut Parser, + or_alter: bool, + ) -> Result { + let name = parser.parse_object_name(false)?; + parser.expect_keyword_is(Keyword::ON)?; + let table_name = parser.parse_object_name(false)?; + let period = parser.parse_trigger_period()?; + let events = parser.parse_comma_separated(Parser::parse_trigger_event)?; + + parser.expect_keyword_is(Keyword::AS)?; + let statements = Some(parser.parse_conditional_statements(&[Keyword::END])?); + + Ok(Statement::CreateTrigger { + or_alter, + or_replace: false, + is_constraint: false, + name, + period, + period_before_table: false, + events, + table_name, + referenced_table_name: None, + referencing: Vec::new(), + trigger_object: TriggerObject::Statement, + include_each: false, + condition: None, + exec_body: None, + statements_as: true, + statements, + characteristics: None, + }) + } + /// Parse a sequence of statements, optionally separated by semicolon. /// /// Stops parsing when reaching EOF or the given keyword. diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index f69e42436..6cf24e14e 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -43,15 +43,19 @@ impl Dialect for MySqlDialect { // See https://dev.mysql.com/doc/refman/8.0/en/identifiers.html. // Identifiers which begin with a digit are recognized while tokenizing numbers, // so they can be distinguished from exponent numeric literals. + // MySQL also implements non ascii utf-8 charecters ch.is_alphabetic() || ch == '_' || ch == '$' || ch == '@' || ('\u{0080}'..='\u{ffff}').contains(&ch) + || !ch.is_ascii() } fn is_identifier_part(&self, ch: char) -> bool { - self.is_identifier_start(ch) || ch.is_ascii_digit() + self.is_identifier_start(ch) || ch.is_ascii_digit() || + // MySQL implements Unicode characters in identifiers. + !ch.is_ascii() } fn is_delimited_identifier_start(&self, ch: char) -> bool { @@ -150,6 +154,10 @@ impl Dialect for MySqlDialect { fn supports_comma_separated_set_assignments(&self) -> bool { true } + + fn supports_data_type_signed_suffix(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 9b08b8f32..e861cc515 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -65,14 +65,15 @@ impl Dialect for PostgreSqlDialect { } fn is_identifier_start(&self, ch: char) -> bool { - // See https://www.postgresql.org/docs/11/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - // We don't yet support identifiers beginning with "letters with - // diacritical marks" - ch.is_alphabetic() || ch == '_' + ch.is_alphabetic() || ch == '_' || + // PostgreSQL implements Unicode characters in identifiers. + !ch.is_ascii() } fn is_identifier_part(&self, ch: char) -> bool { - ch.is_alphabetic() || ch.is_ascii_digit() || ch == '$' || ch == '_' + ch.is_alphabetic() || ch.is_ascii_digit() || ch == '$' || ch == '_' || + // PostgreSQL implements Unicode characters in identifiers. + !ch.is_ascii() } fn supports_unicode_string_literal(&self) -> bool { @@ -104,12 +105,16 @@ impl Dialect for PostgreSqlDialect { fn get_next_precedence(&self, parser: &Parser) -> Option> { let token = parser.peek_token(); - debug!("get_next_precedence() {:?}", token); + debug!("get_next_precedence() {token:?}"); // we only return some custom value here when the behaviour (not merely the numeric value) differs // from the default implementation match token.token { - Token::Word(w) if w.keyword == Keyword::COLLATE => Some(Ok(COLLATE_PREC)), + Token::Word(w) + if w.keyword == Keyword::COLLATE && !parser.in_column_definition_state() => + { + Some(Ok(COLLATE_PREC)) + } Token::LBracket => Some(Ok(BRACKET_PREC)), Token::Arrow | Token::LongArrow @@ -258,4 +263,21 @@ impl Dialect for PostgreSqlDialect { fn supports_set_names(&self) -> bool { true } + + fn supports_alter_column_type_using(&self) -> bool { + true + } + + /// Postgres supports `NOTNULL` as an alias for `IS NOT NULL` + /// See: + fn supports_notnull_operator(&self) -> bool { + true + } + + /// [Postgres] supports optional field and precision options for `INTERVAL` data type. + /// + /// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html + fn supports_interval_options(&self) -> bool { + true + } } diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index feccca5dd..1cd6098a6 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -80,12 +80,14 @@ impl Dialect for RedshiftSqlDialect { } fn is_identifier_start(&self, ch: char) -> bool { - // Extends Postgres dialect with sharp + // UTF-8 multibyte characters are supported in identifiers via the PostgreSqlDialect. + // https://docs.aws.amazon.com/redshift/latest/dg/r_names.html PostgreSqlDialect {}.is_identifier_start(ch) || ch == '#' } fn is_identifier_part(&self, ch: char) -> bool { - // Extends Postgres dialect with sharp + // UTF-8 multibyte characters are supported in identifiers via the PostgreSqlDialect. + // https://docs.aws.amazon.com/redshift/latest/dg/r_names.html PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#' } @@ -129,4 +131,16 @@ impl Dialect for RedshiftSqlDialect { fn supports_string_literal_backslash_escape(&self) -> bool { true } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } + + fn supports_select_exclude(&self) -> bool { + true + } + + fn supports_create_table_like_parenthesized(&self) -> bool { + true + } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index c4d6a5ad5..07ef8317a 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -17,21 +17,25 @@ #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; -use crate::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType, KeyValueOptions}; +use crate::ast::helpers::key_value_options::{ + KeyValueOption, KeyValueOptionType, KeyValueOptions, KeyValueOptionsDelimiter, +}; +use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ - ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, - IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, Statement, TagsColumnOption, - WrappedCollection, + CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry, + CopyIntoSnowflakeKind, CreateTableLikeKind, DollarQuotedString, Ident, IdentityParameters, + IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, + InitializeKind, ObjectName, ObjectNamePart, RefreshModeKind, RowAccessPolicy, ShowObjects, + SqlOption, Statement, StorageSerializationPolicy, TagsColumnOption, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{IsOptional, Parser, ParserError}; -use crate::tokenizer::{Token, Word}; +use crate::tokenizer::Token; #[cfg(not(feature = "std"))] use alloc::boxed::Box; #[cfg(not(feature = "std"))] @@ -42,9 +46,84 @@ use alloc::vec::Vec; use alloc::{format, vec}; use super::keywords::RESERVED_FOR_IDENTIFIER; -use sqlparser::ast::StorageSerializationPolicy; const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT]; + +// See: +const RESERVED_KEYWORDS_FOR_TABLE_FACTOR: &[Keyword] = &[ + Keyword::ALL, + Keyword::ALTER, + Keyword::AND, + Keyword::ANY, + Keyword::AS, + Keyword::BETWEEN, + Keyword::BY, + Keyword::CHECK, + Keyword::COLUMN, + Keyword::CONNECT, + Keyword::CREATE, + Keyword::CROSS, + Keyword::CURRENT, + Keyword::DELETE, + Keyword::DISTINCT, + Keyword::DROP, + Keyword::ELSE, + Keyword::EXISTS, + Keyword::FOLLOWING, + Keyword::FOR, + Keyword::FROM, + Keyword::FULL, + Keyword::GRANT, + Keyword::GROUP, + Keyword::HAVING, + Keyword::ILIKE, + Keyword::IN, + Keyword::INCREMENT, + Keyword::INNER, + Keyword::INSERT, + Keyword::INTERSECT, + Keyword::INTO, + Keyword::IS, + Keyword::JOIN, + Keyword::LEFT, + Keyword::LIKE, + Keyword::MINUS, + Keyword::NATURAL, + Keyword::NOT, + Keyword::NULL, + Keyword::OF, + Keyword::ON, + Keyword::OR, + Keyword::ORDER, + Keyword::QUALIFY, + Keyword::REGEXP, + Keyword::REVOKE, + Keyword::RIGHT, + Keyword::RLIKE, + Keyword::ROW, + Keyword::ROWS, + Keyword::SAMPLE, + Keyword::SELECT, + Keyword::SET, + Keyword::SOME, + Keyword::START, + Keyword::TABLE, + Keyword::TABLESAMPLE, + Keyword::THEN, + Keyword::TO, + Keyword::TRIGGER, + Keyword::UNION, + Keyword::UNIQUE, + Keyword::UPDATE, + Keyword::USING, + Keyword::VALUES, + Keyword::WHEN, + Keyword::WHENEVER, + Keyword::WHERE, + Keyword::WINDOW, + Keyword::WITH, +]; + /// A [`Dialect`] for [Snowflake](https://www.snowflake.com/) #[derive(Debug, Default)] pub struct SnowflakeDialect; @@ -131,6 +210,10 @@ impl Dialect for SnowflakeDialect { } fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.parse_keyword(Keyword::BEGIN) { + return Some(parser.parse_begin_exception_end()); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { // ALTER SESSION let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { @@ -152,6 +235,8 @@ impl Dialect for SnowflakeDialect { _ => None, }; + let dynamic = parser.parse_keyword(Keyword::DYNAMIC); + let mut temporary = false; let mut volatile = false; let mut transient = false; @@ -176,8 +261,10 @@ impl Dialect for SnowflakeDialect { return Some(parse_create_stage(or_replace, temporary, parser)); } else if parser.parse_keyword(Keyword::TABLE) { return Some(parse_create_table( - or_replace, global, temporary, volatile, transient, iceberg, parser, + or_replace, global, temporary, volatile, transient, iceberg, dynamic, parser, )); + } else if parser.parse_keyword(Keyword::DATABASE) { + return Some(parse_create_database(or_replace, transient, parser)); } else { // need to go back with the cursor let mut back = 1; @@ -279,6 +366,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_left_associative_joins_without_parens(&self) -> bool { + false + } + fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { // Unreserve some keywords that Snowflake accepts as identifiers // See: https://docs.snowflake.com/en/sql-reference/reserved-keywords @@ -293,27 +384,28 @@ impl Dialect for SnowflakeDialect { true } - fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { - explicit - || match kw { + fn is_column_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool { + match kw { // The following keywords can be considered an alias as long as // they are not followed by other tokens that may change their meaning // e.g. `SELECT * EXCEPT (col1) FROM tbl` Keyword::EXCEPT - // e.g. `SELECT 1 LIMIT 5` - | Keyword::LIMIT - // e.g. `SELECT 1 OFFSET 5 ROWS` - | Keyword::OFFSET // e.g. `INSERT INTO t SELECT 1 RETURNING *` | Keyword::RETURNING if !matches!(parser.peek_token_ref().token, Token::Comma | Token::EOF) => { false } + // e.g. `SELECT 1 LIMIT 5` - not an alias + // e.g. `SELECT 1 OFFSET 5 ROWS` - not an alias + Keyword::LIMIT | Keyword::OFFSET if peek_for_limit_options(parser) => false, + // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` - // which would give it a different meanings, for example: `SELECT 1 FETCH FIRST 10 ROWS` - not an alias - Keyword::FETCH - if parser.peek_keyword(Keyword::FIRST) || parser.peek_keyword(Keyword::NEXT) => + // which would give it a different meanings, for example: + // `SELECT 1 FETCH FIRST 10 ROWS` - not an alias + // `SELECT 1 FETCH 10` - not an alias + Keyword::FETCH if parser.peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]).is_some() + || peek_for_limit_options(parser) => { false } @@ -338,6 +430,99 @@ impl Dialect for SnowflakeDialect { } } + fn is_table_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool { + match kw { + // The following keywords can be considered an alias as long as + // they are not followed by other tokens that may change their meaning + Keyword::RETURNING + | Keyword::INNER + | Keyword::USING + | Keyword::PIVOT + | Keyword::UNPIVOT + | Keyword::EXCEPT + | Keyword::MATCH_RECOGNIZE + if !matches!(parser.peek_token_ref().token, Token::SemiColon | Token::EOF) => + { + false + } + + // `LIMIT` can be considered an alias as long as it's not followed by a value. For example: + // `SELECT * FROM tbl LIMIT WHERE 1=1` - alias + // `SELECT * FROM tbl LIMIT 3` - not an alias + Keyword::LIMIT | Keyword::OFFSET if peek_for_limit_options(parser) => false, + + // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` + // which would give it a different meanings, for example: + // `SELECT * FROM tbl FETCH FIRST 10 ROWS` - not an alias + // `SELECT * FROM tbl FETCH 10` - not an alias + Keyword::FETCH + if parser + .peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]) + .is_some() + || peek_for_limit_options(parser) => + { + false + } + + // All sorts of join-related keywords can be considered aliases unless additional + // keywords change their meaning. + Keyword::RIGHT | Keyword::LEFT | Keyword::SEMI | Keyword::ANTI + if parser + .peek_one_of_keywords(&[Keyword::JOIN, Keyword::OUTER]) + .is_some() => + { + false + } + + Keyword::GLOBAL if parser.peek_keyword(Keyword::FULL) => false, + + // Reserved keywords by the Snowflake dialect, which seem to be less strictive + // than what is listed in `keywords::RESERVED_FOR_TABLE_ALIAS`. The following + // keywords were tested with the this statement: `SELECT .* FROM tbl `. + Keyword::WITH + | Keyword::ORDER + | Keyword::SELECT + | Keyword::WHERE + | Keyword::GROUP + | Keyword::HAVING + | Keyword::LATERAL + | Keyword::UNION + | Keyword::INTERSECT + | Keyword::MINUS + | Keyword::ON + | Keyword::JOIN + | Keyword::INNER + | Keyword::CROSS + | Keyword::FULL + | Keyword::LEFT + | Keyword::RIGHT + | Keyword::NATURAL + | Keyword::USING + | Keyword::ASOF + | Keyword::MATCH_CONDITION + | Keyword::SET + | Keyword::QUALIFY + | Keyword::FOR + | Keyword::START + | Keyword::CONNECT + | Keyword::SAMPLE + | Keyword::TABLESAMPLE + | Keyword::FROM => false, + + // Any other word is considered an alias + _ => true, + } + } + + fn is_table_factor(&self, kw: &Keyword, parser: &mut Parser) -> bool { + match kw { + Keyword::LIMIT if peek_for_limit_options(parser) => false, + // Table function + Keyword::TABLE if matches!(parser.peek_token_ref().token, Token::LParen) => true, + _ => !RESERVED_KEYWORDS_FOR_TABLE_FACTOR.contains(kw), + } + } + /// See: fn supports_timestamp_versioning(&self) -> bool { true @@ -352,6 +537,51 @@ impl Dialect for SnowflakeDialect { fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] { &RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR } + + fn supports_space_separated_column_options(&self) -> bool { + true + } + + fn supports_comma_separated_drop_column_list(&self) -> bool { + true + } + + fn is_identifier_generating_function_name( + &self, + ident: &Ident, + name_parts: &[ObjectNamePart], + ) -> bool { + ident.quote_style.is_none() + && ident.value.to_lowercase() == "identifier" + && !name_parts + .iter() + .any(|p| matches!(p, ObjectNamePart::Function(_))) + } + + // For example: `SELECT IDENTIFIER('alias1').* FROM tbl AS alias1` + fn supports_select_expr_star(&self) -> bool { + true + } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } + + fn supports_semantic_view_table_factor(&self) -> bool { + true + } +} + +// Peeks ahead to identify tokens that are expected after +// a LIMIT/FETCH keyword. +fn peek_for_limit_options(parser: &Parser) -> bool { + match &parser.peek_token_ref().token { + Token::Number(_, _) | Token::Placeholder(_) => true, + Token::SingleQuotedString(val) if val.is_empty() => true, + Token::DollarQuotedString(DollarQuotedString { value, .. }) if value.is_empty() => true, + Token::Word(w) if w.keyword == Keyword::NULL => true, + _ => false, + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { @@ -382,6 +612,7 @@ fn parse_alter_session(parser: &mut Parser, set: bool) -> Result Result /// +#[allow(clippy::too_many_arguments)] pub fn parse_create_table( or_replace: bool, global: Option, @@ -396,6 +628,7 @@ pub fn parse_create_table( volatile: bool, transient: bool, iceberg: bool, + dynamic: bool, parser: &mut Parser, ) -> Result { let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); @@ -409,6 +642,7 @@ pub fn parse_create_table( .volatile(volatile) .iceberg(iceberg) .global(global) + .dynamic(dynamic) .hive_formats(Some(Default::default())); // Snowflake does not enforce order of the parameters in the statement. The parser needs to @@ -417,6 +651,8 @@ pub fn parse_create_table( // "CREATE TABLE x COPY GRANTS (c INT)" and "CREATE TABLE x (c INT) COPY GRANTS" are both // accepted by Snowflake + let mut plain_options = vec![]; + loop { let next_token = parser.next_token(); match &next_token.token { @@ -428,28 +664,32 @@ pub fn parse_create_table( Keyword::COMMENT => { // Rewind the COMMENT keyword parser.prev_token(); - builder = builder.comment(parser.parse_optional_inline_comment()?); + if let Some(comment_def) = parser.parse_optional_inline_comment()? { + plain_options.push(SqlOption::Comment(comment_def)) + } } Keyword::AS => { let query = parser.parse_query()?; builder = builder.query(Some(query)); - break; } Keyword::CLONE => { let clone = parser.parse_object_name(false).ok(); builder = builder.clone_clause(clone); - break; } Keyword::LIKE => { - let like = parser.parse_object_name(false).ok(); - builder = builder.like(like); - break; + let name = parser.parse_object_name(false)?; + builder = builder.like(Some(CreateTableLikeKind::Plain( + crate::ast::CreateTableLike { + name, + defaults: None, + }, + ))); } Keyword::CLUSTER => { parser.expect_keyword_is(Keyword::BY)?; parser.expect_token(&Token::LParen)?; let cluster_by = Some(WrappedCollection::Parentheses( - parser.parse_comma_separated(|p| p.parse_identifier())?, + parser.parse_comma_separated(|p| p.parse_expr())?, )); parser.expect_token(&Token::RParen)?; @@ -457,29 +697,11 @@ pub fn parse_create_table( } Keyword::ENABLE_SCHEMA_EVOLUTION => { parser.expect_token(&Token::Eq)?; - let enable_schema_evolution = - match parser.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) { - Some(Keyword::TRUE) => true, - Some(Keyword::FALSE) => false, - _ => { - return parser.expected("TRUE or FALSE", next_token); - } - }; - - builder = builder.enable_schema_evolution(Some(enable_schema_evolution)); + builder = builder.enable_schema_evolution(Some(parser.parse_boolean_string()?)); } Keyword::CHANGE_TRACKING => { parser.expect_token(&Token::Eq)?; - let change_tracking = - match parser.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) { - Some(Keyword::TRUE) => true, - Some(Keyword::FALSE) => false, - _ => { - return parser.expected("TRUE or FALSE", next_token); - } - }; - - builder = builder.change_tracking(Some(change_tracking)); + builder = builder.change_tracking(Some(parser.parse_boolean_string()?)); } Keyword::DATA_RETENTION_TIME_IN_DAYS => { parser.expect_token(&Token::Eq)?; @@ -556,6 +778,52 @@ pub fn parse_create_table( builder.storage_serialization_policy = Some(parse_storage_serialization_policy(parser)?); } + Keyword::IF if parser.parse_keywords(&[Keyword::NOT, Keyword::EXISTS]) => { + builder = builder.if_not_exists(true); + } + Keyword::TARGET_LAG => { + parser.expect_token(&Token::Eq)?; + let target_lag = parser.parse_literal_string()?; + builder = builder.target_lag(Some(target_lag)); + } + Keyword::WAREHOUSE => { + parser.expect_token(&Token::Eq)?; + let warehouse = parser.parse_identifier()?; + builder = builder.warehouse(Some(warehouse)); + } + Keyword::AT | Keyword::BEFORE => { + parser.prev_token(); + let version = parser.maybe_parse_table_version()?; + builder = builder.version(version); + } + Keyword::REFRESH_MODE => { + parser.expect_token(&Token::Eq)?; + let refresh_mode = match parser.parse_one_of_keywords(&[ + Keyword::AUTO, + Keyword::FULL, + Keyword::INCREMENTAL, + ]) { + Some(Keyword::AUTO) => Some(RefreshModeKind::Auto), + Some(Keyword::FULL) => Some(RefreshModeKind::Full), + Some(Keyword::INCREMENTAL) => Some(RefreshModeKind::Incremental), + _ => return parser.expected("AUTO, FULL or INCREMENTAL", next_token), + }; + builder = builder.refresh_mode(refresh_mode); + } + Keyword::INITIALIZE => { + parser.expect_token(&Token::Eq)?; + let initialize = match parser + .parse_one_of_keywords(&[Keyword::ON_CREATE, Keyword::ON_SCHEDULE]) + { + Some(Keyword::ON_CREATE) => Some(InitializeKind::OnCreate), + Some(Keyword::ON_SCHEDULE) => Some(InitializeKind::OnSchedule), + _ => return parser.expected("ON_CREATE or ON_SCHEDULE", next_token), + }; + builder = builder.initialize(initialize); + } + Keyword::REQUIRE if parser.parse_keyword(Keyword::USER) => { + builder = builder.require_user(true); + } _ => { return parser.expected("end of statement", next_token); } @@ -566,21 +834,9 @@ pub fn parse_create_table( builder = builder.columns(columns).constraints(constraints); } Token::EOF => { - if builder.columns.is_empty() { - return Err(ParserError::ParserError( - "unexpected end of input".to_string(), - )); - } - break; } Token::SemiColon => { - if builder.columns.is_empty() { - return Err(ParserError::ParserError( - "unexpected end of input".to_string(), - )); - } - parser.prev_token(); break; } @@ -589,6 +845,13 @@ pub fn parse_create_table( } } } + let table_options = if !plain_options.is_empty() { + crate::ast::CreateTableOptions::Plain(plain_options) + } else { + crate::ast::CreateTableOptions::None + }; + + builder = builder.table_options(table_options); if iceberg && builder.base_location.is_none() { return Err(ParserError::ParserError( @@ -599,6 +862,115 @@ pub fn parse_create_table( Ok(builder.build()) } +/// Parse snowflake create database statement. +/// +pub fn parse_create_database( + or_replace: bool, + transient: bool, + parser: &mut Parser, +) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + + let mut builder = CreateDatabaseBuilder::new(name) + .or_replace(or_replace) + .transient(transient) + .if_not_exists(if_not_exists); + + loop { + let next_token = parser.next_token(); + match &next_token.token { + Token::Word(word) => match word.keyword { + Keyword::CLONE => { + builder = builder.clone_clause(Some(parser.parse_object_name(false)?)); + } + Keyword::DATA_RETENTION_TIME_IN_DAYS => { + parser.expect_token(&Token::Eq)?; + builder = + builder.data_retention_time_in_days(Some(parser.parse_literal_uint()?)); + } + Keyword::MAX_DATA_EXTENSION_TIME_IN_DAYS => { + parser.expect_token(&Token::Eq)?; + builder = + builder.max_data_extension_time_in_days(Some(parser.parse_literal_uint()?)); + } + Keyword::EXTERNAL_VOLUME => { + parser.expect_token(&Token::Eq)?; + builder = builder.external_volume(Some(parser.parse_literal_string()?)); + } + Keyword::CATALOG => { + parser.expect_token(&Token::Eq)?; + builder = builder.catalog(Some(parser.parse_literal_string()?)); + } + Keyword::REPLACE_INVALID_CHARACTERS => { + parser.expect_token(&Token::Eq)?; + builder = + builder.replace_invalid_characters(Some(parser.parse_boolean_string()?)); + } + Keyword::DEFAULT_DDL_COLLATION => { + parser.expect_token(&Token::Eq)?; + builder = builder.default_ddl_collation(Some(parser.parse_literal_string()?)); + } + Keyword::STORAGE_SERIALIZATION_POLICY => { + parser.expect_token(&Token::Eq)?; + let policy = parse_storage_serialization_policy(parser)?; + builder = builder.storage_serialization_policy(Some(policy)); + } + Keyword::COMMENT => { + parser.expect_token(&Token::Eq)?; + builder = builder.comment(Some(parser.parse_literal_string()?)); + } + Keyword::CATALOG_SYNC => { + parser.expect_token(&Token::Eq)?; + builder = builder.catalog_sync(Some(parser.parse_literal_string()?)); + } + Keyword::CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER => { + parser.expect_token(&Token::Eq)?; + builder = builder.catalog_sync_namespace_flatten_delimiter(Some( + parser.parse_literal_string()?, + )); + } + Keyword::CATALOG_SYNC_NAMESPACE_MODE => { + parser.expect_token(&Token::Eq)?; + let mode = + match parser.parse_one_of_keywords(&[Keyword::NEST, Keyword::FLATTEN]) { + Some(Keyword::NEST) => CatalogSyncNamespaceMode::Nest, + Some(Keyword::FLATTEN) => CatalogSyncNamespaceMode::Flatten, + _ => { + return parser.expected("NEST or FLATTEN", next_token); + } + }; + builder = builder.catalog_sync_namespace_mode(Some(mode)); + } + Keyword::WITH => { + if parser.parse_keyword(Keyword::TAG) { + parser.expect_token(&Token::LParen)?; + let tags = parser.parse_comma_separated(Parser::parse_tag)?; + parser.expect_token(&Token::RParen)?; + builder = builder.with_tags(Some(tags)); + } else if parser.parse_keyword(Keyword::CONTACT) { + parser.expect_token(&Token::LParen)?; + let contacts = parser.parse_comma_separated(|p| { + let purpose = p.parse_identifier()?.value; + p.expect_token(&Token::Eq)?; + let contact = p.parse_identifier()?.value; + Ok(ContactEntry { purpose, contact }) + })?; + parser.expect_token(&Token::RParen)?; + builder = builder.with_contacts(Some(contacts)); + } else { + return parser.expected("TAG or CONTACT", next_token); + } + } + _ => return parser.expected("end of statement", next_token), + }, + Token::SemiColon | Token::EOF => break, + _ => return parser.expected("end of statement", next_token), + } + } + Ok(builder.build()) +} + pub fn parse_storage_serialization_policy( parser: &mut Parser, ) -> Result { @@ -632,19 +1004,19 @@ pub fn parse_create_stage( // [ directoryTableParams ] if parser.parse_keyword(Keyword::DIRECTORY) { parser.expect_token(&Token::Eq)?; - directory_table_params = parse_parentheses_options(parser)?; + directory_table_params = parser.parse_key_value_options(true, &[])?; } // [ file_format] if parser.parse_keyword(Keyword::FILE_FORMAT) { parser.expect_token(&Token::Eq)?; - file_format = parse_parentheses_options(parser)?; + file_format = parser.parse_key_value_options(true, &[])?; } // [ copy_options ] if parser.parse_keyword(Keyword::COPY_OPTIONS) { parser.expect_token(&Token::Eq)?; - copy_options = parse_parentheses_options(parser)?; + copy_options = parser.parse_key_value_options(true, &[])?; } // [ comment ] @@ -661,12 +1033,15 @@ pub fn parse_create_stage( stage_params, directory_table_params: KeyValueOptions { options: directory_table_params, + delimiter: KeyValueOptionsDelimiter::Space, }, file_format: KeyValueOptions { options: file_format, + delimiter: KeyValueOptionsDelimiter::Space, }, copy_options: KeyValueOptions { options: copy_options, + delimiter: KeyValueOptionsDelimiter::Space, }, comment, }) @@ -689,6 +1064,8 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result ident.push('~'), Token::Mod => ident.push('%'), Token::Div => ident.push('/'), + Token::Plus => ident.push('+'), + Token::Number(n, _) => ident.push_str(n), Token::Word(w) => ident.push_str(&w.to_string()), _ => return parser.expected("stage name identifier", parser.peek_token()), } @@ -733,10 +1110,16 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { let mut from_stage = None; let mut stage_params = StageParamsObject { url: None, - encryption: KeyValueOptions { options: vec![] }, + encryption: KeyValueOptions { + options: vec![], + delimiter: KeyValueOptionsDelimiter::Space, + }, endpoint: None, storage_integration: None, - credentials: KeyValueOptions { options: vec![] }, + credentials: KeyValueOptions { + options: vec![], + delimiter: KeyValueOptionsDelimiter::Space, + }, }; let mut from_query = None; let mut partition = None; @@ -798,7 +1181,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { // FILE_FORMAT if parser.parse_keyword(Keyword::FILE_FORMAT) { parser.expect_token(&Token::Eq)?; - file_format = parse_parentheses_options(parser)?; + file_format = parser.parse_key_value_options(true, &[])?; // PARTITION BY } else if parser.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { partition = Some(Box::new(parser.parse_expr()?)) @@ -836,14 +1219,14 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { // COPY OPTIONS } else if parser.parse_keyword(Keyword::COPY_OPTIONS) { parser.expect_token(&Token::Eq)?; - copy_options = parse_parentheses_options(parser)?; + copy_options = parser.parse_key_value_options(true, &[])?; } else { match parser.next_token().token { Token::SemiColon | Token::EOF => break, Token::Comma => continue, // In `COPY INTO ` the copy options do not have a shared key // like in `COPY INTO
` - Token::Word(key) => copy_options.push(parse_option(parser, key)?), + Token::Word(key) => copy_options.push(parser.parse_key_value_option(key)?), _ => return parser.expected("another copy option, ; or EOF'", parser.peek_token()), } } @@ -862,9 +1245,11 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { pattern, file_format: KeyValueOptions { options: file_format, + delimiter: KeyValueOptionsDelimiter::Space, }, copy_options: KeyValueOptions { options: copy_options, + delimiter: KeyValueOptionsDelimiter::Space, }, validation_mode, partition, @@ -964,8 +1349,14 @@ fn parse_select_item_for_data_load( fn parse_stage_params(parser: &mut Parser) -> Result { let (mut url, mut storage_integration, mut endpoint) = (None, None, None); - let mut encryption: KeyValueOptions = KeyValueOptions { options: vec![] }; - let mut credentials: KeyValueOptions = KeyValueOptions { options: vec![] }; + let mut encryption: KeyValueOptions = KeyValueOptions { + options: vec![], + delimiter: KeyValueOptionsDelimiter::Space, + }; + let mut credentials: KeyValueOptions = KeyValueOptions { + options: vec![], + delimiter: KeyValueOptionsDelimiter::Space, + }; // URL if parser.parse_keyword(Keyword::URL) { @@ -995,7 +1386,8 @@ fn parse_stage_params(parser: &mut Parser) -> Result Result { parser.advance_token(); if set { - let option = parse_option(parser, key)?; + let option = parser.parse_key_value_option(key)?; options.push(option); } else { options.push(KeyValueOption { @@ -1061,63 +1454,6 @@ fn parse_session_options( } } -/// Parses options provided within parentheses like: -/// ( ENABLE = { TRUE | FALSE } -/// [ AUTO_REFRESH = { TRUE | FALSE } ] -/// [ REFRESH_ON_CREATE = { TRUE | FALSE } ] -/// [ NOTIFICATION_INTEGRATION = '' ] ) -/// -fn parse_parentheses_options(parser: &mut Parser) -> Result, ParserError> { - let mut options: Vec = Vec::new(); - parser.expect_token(&Token::LParen)?; - loop { - match parser.next_token().token { - Token::RParen => break, - Token::Comma => continue, - Token::Word(key) => options.push(parse_option(parser, key)?), - _ => return parser.expected("another option or ')'", parser.peek_token()), - }; - } - Ok(options) -} - -/// Parses a `KEY = VALUE` construct based on the specified key -fn parse_option(parser: &mut Parser, key: Word) -> Result { - parser.expect_token(&Token::Eq)?; - if parser.parse_keyword(Keyword::TRUE) { - Ok(KeyValueOption { - option_name: key.value, - option_type: KeyValueOptionType::BOOLEAN, - value: "TRUE".to_string(), - }) - } else if parser.parse_keyword(Keyword::FALSE) { - Ok(KeyValueOption { - option_name: key.value, - option_type: KeyValueOptionType::BOOLEAN, - value: "FALSE".to_string(), - }) - } else { - match parser.next_token().token { - Token::SingleQuotedString(value) => Ok(KeyValueOption { - option_name: key.value, - option_type: KeyValueOptionType::STRING, - value, - }), - Token::Word(word) => Ok(KeyValueOption { - option_name: key.value, - option_type: KeyValueOptionType::ENUM, - value: word.value, - }), - Token::Number(n, _) => Ok(KeyValueOption { - option_name: key.value, - option_type: KeyValueOptionType::NUMBER, - value: n, - }), - _ => parser.expected("expected option value", parser.peek_token()), - } - } -} - /// Parsing a property of identity or autoincrement column option /// Syntax: /// ```sql @@ -1163,7 +1499,7 @@ fn parse_column_policy_property( parser: &mut Parser, with: bool, ) -> Result { - let policy_name = parser.parse_identifier()?; + let policy_name = parser.parse_object_name(false)?; let using_columns = if parser.parse_keyword(Keyword::USING) { parser.expect_token(&Token::LParen)?; let columns = parser.parse_comma_separated(|p| p.parse_identifier())?; diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 138c4692c..64a8d532f 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -15,7 +15,11 @@ // specific language governing permissions and limitations // under the License. -use crate::ast::Statement; +#[cfg(not(feature = "std"))] +use alloc::boxed::Box; + +use crate::ast::BinaryOperator; +use crate::ast::{Expr, Statement}; use crate::dialect::Dialect; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -70,6 +74,27 @@ impl Dialect for SQLiteDialect { } } + fn parse_infix( + &self, + parser: &mut crate::parser::Parser, + expr: &crate::ast::Expr, + _precedence: u8, + ) -> Option> { + // Parse MATCH and REGEXP as operators + // See + for (keyword, op) in [ + (Keyword::REGEXP, BinaryOperator::Regexp), + (Keyword::MATCH, BinaryOperator::Match), + ] { + if parser.parse_keyword(keyword) { + let left = Box::new(expr.clone()); + let right = Box::new(parser.parse_expr().unwrap()); + return Some(Ok(Expr::BinaryOp { left, op, right })); + } + } + None + } + fn supports_in_empty_list(&self) -> bool { true } @@ -85,4 +110,10 @@ impl Dialect for SQLiteDialect { fn supports_dollar_placeholder(&self) -> bool { true } + + /// SQLite supports `NOTNULL` as aliases for `IS NOT NULL` + /// See: + fn supports_notnull_operator(&self) -> bool { + true + } } diff --git a/src/display_utils.rs b/src/display_utils.rs new file mode 100644 index 000000000..ba36fccdd --- /dev/null +++ b/src/display_utils.rs @@ -0,0 +1,135 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +//! Utilities for formatting SQL AST nodes with pretty printing support. +//! +//! The module provides formatters that implement the `Display` trait with support +//! for both regular (`{}`) and pretty (`{:#}`) formatting modes. Pretty printing +//! adds proper indentation and line breaks to make SQL statements more readable. + +use core::fmt::{self, Display, Write}; + +/// A wrapper around a value that adds an indent to the value when displayed with {:#}. +pub(crate) struct Indent(pub T); + +const INDENT: &str = " "; + +impl Display for Indent +where + T: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + f.write_str(INDENT)?; + write!(Indent(f), "{:#}", self.0) + } else { + self.0.fmt(f) + } + } +} + +/// Adds an indent to the inner writer +impl Write for Indent +where + T: Write, +{ + fn write_str(&mut self, s: &str) -> fmt::Result { + self.0.write_str(s)?; + // Our NewLine and SpaceOrNewline utils always print individual newlines as a single-character string. + if s == "\n" { + self.0.write_str(INDENT)?; + } + Ok(()) + } +} + +/// A value that inserts a newline when displayed with {:#}, but not when displayed with {}. +pub(crate) struct NewLine; + +impl Display for NewLine { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + f.write_char('\n') + } else { + Ok(()) + } + } +} + +/// A value that inserts a space when displayed with {}, but a newline when displayed with {:#}. +pub(crate) struct SpaceOrNewline; + +impl Display for SpaceOrNewline { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + f.write_char('\n') + } else { + f.write_char(' ') + } + } +} + +/// A value that displays a comma-separated list of values. +/// When pretty-printed (using {:#}), it displays each value on a new line. +pub(crate) struct DisplayCommaSeparated<'a, T: fmt::Display>(pub(crate) &'a [T]); + +impl fmt::Display for DisplayCommaSeparated<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut first = true; + for t in self.0 { + if !first { + f.write_char(',')?; + SpaceOrNewline.fmt(f)?; + } + first = false; + t.fmt(f)?; + } + Ok(()) + } +} + +/// Displays a whitespace, followed by a comma-separated list that is indented when pretty-printed. +pub(crate) fn indented_list(f: &mut fmt::Formatter, items: &[T]) -> fmt::Result { + SpaceOrNewline.fmt(f)?; + Indent(DisplayCommaSeparated(items)).fmt(f) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_indent() { + struct TwoLines; + + impl Display for TwoLines { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("line 1")?; + SpaceOrNewline.fmt(f)?; + f.write_str("line 2") + } + } + + let indent = Indent(TwoLines); + assert_eq!( + indent.to_string(), + TwoLines.to_string(), + "Only the alternate form should be indented" + ); + assert_eq!(format!("{:#}", indent), " line 1\n line 2"); + } +} diff --git a/src/keywords.rs b/src/keywords.rs index 15a6f91ad..d78a6c178 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -76,6 +76,8 @@ define_keywords!( ABS, ABSENT, ABSOLUTE, + ACCEPTANYDATE, + ACCEPTINVCHARS, ACCESS, ACCOUNT, ACTION, @@ -83,6 +85,7 @@ define_keywords!( ADMIN, AFTER, AGAINST, + AGGREGATE, AGGREGATION, ALERT, ALGORITHM, @@ -115,9 +118,11 @@ define_keywords!( AUTHENTICATION, AUTHORIZATION, AUTO, + AUTOEXTEND_SIZE, AUTOINCREMENT, AUTO_INCREMENT, AVG, + AVG_ROW_LENGTH, AVRO, BACKWARD, BASE64, @@ -135,12 +140,14 @@ define_keywords!( BIND, BINDING, BIT, + BLANKSASNULL, BLOB, BLOCK, BLOOM, BLOOMFILTER, BOOL, BOOLEAN, + BOOST, BOTH, BOX, BRIN, @@ -163,6 +170,8 @@ define_keywords!( CAST, CATALOG, CATALOG_SYNC, + CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER, + CATALOG_SYNC_NAMESPACE_MODE, CATCH, CEIL, CEILING, @@ -179,6 +188,7 @@ define_keywords!( CHARSET, CHAR_LENGTH, CHECK, + CHECKSUM, CIRCLE, CLEAR, CLOB, @@ -209,6 +219,7 @@ define_keywords!( CONNECTOR, CONNECT_BY_ROOT, CONSTRAINT, + CONTACT, CONTAINS, CONTINUE, CONVERT, @@ -247,6 +258,7 @@ define_keywords!( DATA_RETENTION_TIME_IN_DAYS, DATE, DATE32, + DATEFORMAT, DATETIME, DATETIME64, DAY, @@ -261,6 +273,7 @@ define_keywords!( DECLARE, DEDUPLICATE, DEFAULT, + DEFAULTS, DEFAULT_DDL_COLLATION, DEFERRABLE, DEFERRED, @@ -268,17 +281,20 @@ define_keywords!( DEFINED, DEFINER, DELAYED, + DELAY_KEY_WRITE, DELETE, DELIMITED, DELIMITER, DELTA, DENSE_RANK, + DENY, DEREF, DESC, DESCRIBE, DETACH, DETAIL, DETERMINISTIC, + DIMENSIONS, DIRECTORY, DISABLE, DISCARD, @@ -287,8 +303,10 @@ define_keywords!( DISTRIBUTE, DIV, DO, + DOMAIN, DOUBLE, DOW, + DOWNSTREAM, DOY, DROP, DRY, @@ -300,6 +318,7 @@ define_keywords!( ELSE, ELSEIF, EMPTY, + EMPTYASNULL, ENABLE, ENABLE_SCHEMA_EVOLUTION, ENCODING, @@ -311,6 +330,7 @@ define_keywords!( END_PARTITION, ENFORCED, ENGINE, + ENGINE_ATTRIBUTE, ENUM, ENUM16, ENUM8, @@ -328,6 +348,7 @@ define_keywords!( EXCEPTION, EXCHANGE, EXCLUDE, + EXCLUDING, EXCLUSIVE, EXEC, EXECUTE, @@ -338,11 +359,13 @@ define_keywords!( EXPLAIN, EXPLICIT, EXPORT, + EXTEND, EXTENDED, EXTENSION, EXTERNAL, EXTERNAL_VOLUME, EXTRACT, + FACTS, FAIL, FAILOVER, FALSE, @@ -357,6 +380,7 @@ define_keywords!( FIRST, FIRST_VALUE, FIXEDSTRING, + FLATTEN, FLOAT, FLOAT32, FLOAT4, @@ -386,6 +410,7 @@ define_keywords!( FUNCTION, FUNCTIONS, FUSION, + FUTURE, GENERAL, GENERATE, GENERATED, @@ -413,12 +438,14 @@ define_keywords!( HOUR, HOURS, HUGEINT, + IAM_ROLE, ICEBERG, ID, IDENTITY, IDENTITY_INSERT, IF, IGNORE, + IGNOREHEADER, ILIKE, IMMEDIATE, IMMUTABLE, @@ -427,11 +454,14 @@ define_keywords!( IN, INCLUDE, INCLUDE_NULL_VALUES, + INCLUDING, INCREMENT, + INCREMENTAL, INDEX, INDICATOR, INHERIT, INHERITS, + INITIALIZE, INITIALLY, INNER, INOUT, @@ -441,6 +471,7 @@ define_keywords!( INPUTFORMAT, INSENSITIVE, INSERT, + INSERT_METHOD, INSTALL, INSTANT, INSTEAD, @@ -477,6 +508,7 @@ define_keywords!( JULIAN, KEY, KEYS, + KEY_BLOCK_SIZE, KILL, LAG, LANGUAGE, @@ -530,6 +562,7 @@ define_keywords!( MAX, MAXVALUE, MAX_DATA_EXTENSION_TIME_IN_DAYS, + MAX_ROWS, MEASURES, MEDIUMBLOB, MEDIUMINT, @@ -540,6 +573,7 @@ define_keywords!( METADATA, METHOD, METRIC, + METRICS, MICROSECOND, MICROSECONDS, MILLENIUM, @@ -551,6 +585,7 @@ define_keywords!( MINUTE, MINUTES, MINVALUE, + MIN_ROWS, MOD, MODE, MODIFIES, @@ -570,6 +605,7 @@ define_keywords!( NATURAL, NCHAR, NCLOB, + NEST, NESTED, NETWORK, NEW, @@ -594,6 +630,7 @@ define_keywords!( NOT, NOTHING, NOTIFY, + NOTNULL, NOWAIT, NO_WRITE_TO_BINLOG, NTH_VALUE, @@ -618,6 +655,8 @@ define_keywords!( ON, ONE, ONLY, + ON_CREATE, + ON_SCHEDULE, OPEN, OPENJSON, OPERATE, @@ -633,6 +672,7 @@ define_keywords!( ORDER, ORDINALITY, ORGANIZATION, + OTHER, OUT, OUTER, OUTPUT, @@ -648,6 +688,7 @@ define_keywords!( OWNERSHIP, PACKAGE, PACKAGES, + PACK_KEYS, PARALLEL, PARAMETER, PARQUET, @@ -717,6 +758,7 @@ define_keywords!( REF, REFERENCES, REFERENCING, + REFRESH_MODE, REGCLASS, REGEXP, REGR_AVGX, @@ -728,6 +770,7 @@ define_keywords!( REGR_SXX, REGR_SXY, REGR_SYY, + REINDEX, RELATIVE, RELAY, RELEASE, @@ -739,9 +782,11 @@ define_keywords!( REPAIR, REPEATABLE, REPLACE, + REPLACE_INVALID_CHARACTERS, REPLICA, REPLICATE, REPLICATION, + REQUIRE, RESET, RESOLVE, RESOURCE, @@ -770,6 +815,7 @@ define_keywords!( ROW, ROWID, ROWS, + ROW_FORMAT, ROW_NUMBER, RULE, RUN, @@ -784,11 +830,13 @@ define_keywords!( SEARCH, SECOND, SECONDARY, + SECONDARY_ENGINE_ATTRIBUTE, SECONDS, SECRET, SECURITY, SEED, SELECT, + SEMANTIC_VIEW, SEMI, SENSITIVE, SEPARATOR, @@ -798,6 +846,7 @@ define_keywords!( SERDE, SERDEPROPERTIES, SERIALIZABLE, + SERVER, SERVICE, SESSION, SESSION_USER, @@ -828,6 +877,7 @@ define_keywords!( SQLSTATE, SQLWARNING, SQRT, + SRID, STABLE, STAGE, START, @@ -835,16 +885,21 @@ define_keywords!( STATEMENT, STATIC, STATISTICS, + STATS_AUTO_RECALC, + STATS_PERSISTENT, + STATS_SAMPLE_PAGES, STATUS, STDDEV_POP, STDDEV_SAMP, STDIN, STDOUT, STEP, + STORAGE, STORAGE_INTEGRATION, STORAGE_SERIALIZATION_POLICY, STORED, STRAIGHT_JOIN, + STREAM, STRICT, STRING, STRUCT, @@ -867,8 +922,10 @@ define_keywords!( TABLE, TABLES, TABLESAMPLE, + TABLESPACE, TAG, TARGET, + TARGET_LAG, TASK, TBLPROPERTIES, TEMP, @@ -881,6 +938,7 @@ define_keywords!( THEN, TIES, TIME, + TIMEFORMAT, TIMESTAMP, TIMESTAMPTZ, TIMESTAMP_NTZ, @@ -909,9 +967,12 @@ define_keywords!( TRIM_ARRAY, TRUE, TRUNCATE, + TRUNCATECOLUMNS, TRY, TRY_CAST, TRY_CONVERT, + TSQUERY, + TSVECTOR, TUPLE, TYPE, UBIGINT, @@ -955,6 +1016,7 @@ define_keywords!( UUID, VACUUM, VALID, + VALIDATE, VALIDATION_MODE, VALUE, VALUES, @@ -982,6 +1044,7 @@ define_keywords!( WHEN, WHENEVER, WHERE, + WHILE, WIDTH_BUCKET, WINDOW, WITH, @@ -989,6 +1052,7 @@ define_keywords!( WITHOUT, WITHOUT_ARRAY_WRAPPER, WORK, + WRAPPER, WRITE, XML, XMLNAMESPACES, @@ -1065,6 +1129,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::SAMPLE, Keyword::TABLESAMPLE, Keyword::FROM, + Keyword::OPEN, ]; /// Can't be used as a column alias, so that `SELECT alias` @@ -1088,6 +1153,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::FETCH, Keyword::UNION, Keyword::EXCEPT, + Keyword::EXCLUDE, Keyword::INTERSECT, Keyword::MINUS, Keyword::CLUSTER, diff --git a/src/lib.rs b/src/lib.rs index 5d72f9f0e..dbfd1791a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,27 @@ //! // The original SQL text can be generated from the AST //! assert_eq!(ast[0].to_string(), sql); //! ``` +//! +//! # Pretty Printing +//! +//! SQL statements can be pretty-printed with proper indentation and line breaks using the alternate flag (`{:#}`): +//! +//! ``` +//! # use sqlparser::dialect::GenericDialect; +//! # use sqlparser::parser::Parser; +//! let sql = "SELECT a, b FROM table_1"; +//! let ast = Parser::parse_sql(&GenericDialect, sql).unwrap(); +//! +//! // Pretty print with indentation and line breaks +//! let pretty_sql = format!("{:#}", ast[0]); +//! assert_eq!(pretty_sql, r#" +//! SELECT +//! a, +//! b +//! FROM +//! table_1 +//! "#.trim()); +//! ``` //! [sqlparser crates.io page]: https://crates.io/crates/sqlparser //! [`Parser::parse_sql`]: crate::parser::Parser::parse_sql //! [`Parser::new`]: crate::parser::Parser::new @@ -128,6 +149,10 @@ #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::upper_case_acronyms)] +// Permit large enum variants to keep a unified, expressive AST. +// Splitting complex nodes (expressions, statements, types) into separate types +// would bloat the API and hide intent. Extra memory is a worthwhile tradeoff. +#![allow(clippy::large_enum_variant)] // Allow proc-macros to find this crate extern crate self as sqlparser; @@ -142,6 +167,7 @@ extern crate pretty_assertions; pub mod ast; #[macro_use] pub mod dialect; +mod display_utils; pub mod keywords; pub mod parser; pub mod tokenizer; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 03ea91faf..6c559eed4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -32,12 +32,18 @@ use recursion::RecursionCounter; use IsLateral::*; use IsOptional::*; -use crate::ast::helpers::stmt_create_table::{CreateTableBuilder, CreateTableConfiguration}; +use crate::ast::helpers::{ + key_value_options::{ + KeyValueOption, KeyValueOptionType, KeyValueOptions, KeyValueOptionsDelimiter, + }, + stmt_create_table::{CreateTableBuilder, CreateTableConfiguration}, +}; use crate::ast::Statement::CreatePolicy; use crate::ast::*; use crate::dialect::*; use crate::keywords::{Keyword, ALL_KEYWORDS}; use crate::tokenizer::*; +use sqlparser::parser::ParserState::ColumnDefinition; mod alter; @@ -222,6 +228,9 @@ pub struct ParserOptions { /// Controls how literal values are unescaped. See /// [`Tokenizer::with_unescape`] for more details. pub unescape: bool, + /// Controls if the parser expects a semi-colon token + /// between statements. Default is `true`. + pub require_semicolon_stmt_delimiter: bool, } impl Default for ParserOptions { @@ -229,6 +238,7 @@ impl Default for ParserOptions { Self { trailing_commas: false, unescape: true, + require_semicolon_stmt_delimiter: true, } } } @@ -271,6 +281,12 @@ enum ParserState { /// PRIOR expressions while still allowing prior as an identifier name /// in other contexts. ConnectBy, + /// The state when parsing column definitions. This state prohibits + /// NOT NULL as an alias for IS NOT NULL. For example: + /// ```sql + /// CREATE TABLE foo (abc BIGINT NOT NULL); + /// ``` + ColumnDefinition, } /// A SQL Parser @@ -436,7 +452,7 @@ impl<'a> Parser<'a> { /// /// See example on [`Parser::new()`] for an example pub fn try_with_sql(self, sql: &str) -> Result { - debug!("Parsing sql '{}'...", sql); + debug!("Parsing sql '{sql}'..."); let tokens = Tokenizer::new(self.dialect, sql) .with_unescape(self.options.unescape) .tokenize_with_location()?; @@ -467,6 +483,10 @@ impl<'a> Parser<'a> { expecting_statement_delimiter = false; } + if !self.options.require_semicolon_stmt_delimiter { + expecting_statement_delimiter = false; + } + match self.peek_token().token { Token::EOF => break, @@ -536,6 +556,10 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_if_stmt() } + Keyword::WHILE => { + self.prev_token(); + self.parse_while() + } Keyword::RAISE => { self.prev_token(); self.parse_raise_stmt() @@ -570,11 +594,19 @@ impl<'a> Parser<'a> { Keyword::ALTER => self.parse_alter(), Keyword::CALL => self.parse_call(), Keyword::COPY => self.parse_copy(), + Keyword::OPEN => { + self.prev_token(); + self.parse_open() + } Keyword::CLOSE => self.parse_close(), Keyword::SET => self.parse_set(), Keyword::SHOW => self.parse_show(), Keyword::USE => self.parse_use(), Keyword::GRANT => self.parse_grant(), + Keyword::DENY => { + self.prev_token(); + self.parse_deny() + } Keyword::REVOKE => self.parse_revoke(), Keyword::START => self.parse_start_transaction(), Keyword::BEGIN => self.parse_begin(), @@ -613,6 +645,14 @@ impl<'a> Parser<'a> { Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(), Keyword::PRINT => self.parse_print(), Keyword::RETURN => self.parse_return(), + Keyword::EXPORT => { + self.prev_token(); + self.parse_export_data() + } + Keyword::VACUUM => { + self.prev_token(); + self.parse_vacuum() + } _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -700,8 +740,18 @@ impl<'a> Parser<'a> { })) } + /// Parse a `WHILE` statement. + /// + /// See [Statement::While] + fn parse_while(&mut self) -> Result { + self.expect_keyword_is(Keyword::WHILE)?; + let while_block = self.parse_conditional_statement_block(&[Keyword::END])?; + + Ok(Statement::While(WhileStatement { while_block })) + } + /// Parses an expression and associated list of statements - /// belonging to a conditional statement like `IF` or `WHEN`. + /// belonging to a conditional statement like `IF` or `WHEN` or `WHILE`. /// /// Example: /// ```sql @@ -716,6 +766,10 @@ impl<'a> Parser<'a> { let condition = match &start_token.token { Token::Word(w) if w.keyword == Keyword::ELSE => None, + Token::Word(w) if w.keyword == Keyword::WHILE => { + let expr = self.parse_expr()?; + Some(expr) + } _ => { let expr = self.parse_expr()?; then_token = Some(AttachedToken(self.expect_keyword(Keyword::THEN)?)); @@ -723,16 +777,40 @@ impl<'a> Parser<'a> { } }; - let statements = self.parse_statement_list(terminal_keywords)?; + let conditional_statements = self.parse_conditional_statements(terminal_keywords)?; Ok(ConditionalStatementBlock { start_token: AttachedToken(start_token), condition, then_token, - conditional_statements: ConditionalStatements::Sequence { statements }, + conditional_statements, }) } + /// Parse a BEGIN/END block or a sequence of statements + /// This could be inside of a conditional (IF, CASE, WHILE etc.) or an object body defined optionally BEGIN/END and one or more statements. + pub(crate) fn parse_conditional_statements( + &mut self, + terminal_keywords: &[Keyword], + ) -> Result { + let conditional_statements = if self.peek_keyword(Keyword::BEGIN) { + let begin_token = self.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(terminal_keywords)?; + let end_token = self.expect_keyword(Keyword::END)?; + + ConditionalStatements::BeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + }) + } else { + ConditionalStatements::Sequence { + statements: self.parse_statement_list(terminal_keywords)?, + } + }; + Ok(conditional_statements) + } + /// Parse a `RAISE` statement. /// /// See [Statement::Raise] @@ -910,12 +988,13 @@ impl<'a> Parser<'a> { pub fn parse_truncate(&mut self) -> Result { let table = self.parse_keyword(Keyword::TABLE); - let only = self.parse_keyword(Keyword::ONLY); let table_names = self - .parse_comma_separated(|p| p.parse_object_name(false))? + .parse_comma_separated(|p| { + Ok((p.parse_keyword(Keyword::ONLY), p.parse_object_name(false)?)) + })? .into_iter() - .map(|n| TruncateTableTarget { name: n }) + .map(|(only, name)| TruncateTableTarget { name, only }) .collect(); let mut partitions = None; @@ -946,7 +1025,6 @@ impl<'a> Parser<'a> { table_names, partitions, table, - only, identity, cascade, on_cluster, @@ -1149,6 +1227,25 @@ impl<'a> Parser<'a> { self.parse_subexpr(self.dialect.prec_unknown()) } + pub fn parse_expr_with_alias_and_order_by( + &mut self, + ) -> Result { + let expr = self.parse_expr()?; + + fn validator(explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { + explicit || !&[Keyword::ASC, Keyword::DESC, Keyword::GROUP].contains(kw) + } + let alias = self.parse_optional_alias_inner(None, validator)?; + let order_by = OrderByOptions { + asc: self.parse_asc_desc(), + nulls_first: None, + }; + Ok(ExprWithAliasAndOrderBy { + expr: ExprWithAlias { expr, alias }, + order_by, + }) + } + /// Parse tokens until the precedence changes. pub fn parse_subexpr(&mut self, precedence: u8) -> Result { let _guard = self.recursion_counter.try_decrease()?; @@ -1157,10 +1254,10 @@ impl<'a> Parser<'a> { expr = self.parse_compound_expr(expr, vec![])?; - debug!("prefix: {:?}", expr); + debug!("prefix: {expr:?}"); loop { let next_precedence = self.get_next_precedence()?; - debug!("next precedence: {:?}", next_precedence); + debug!("next precedence: {next_precedence:?}"); if precedence >= next_precedence { break; @@ -1441,7 +1538,7 @@ impl<'a> Parser<'a> { let loc = self.peek_token_ref().span.start; let opt_expr = self.maybe_parse(|parser| { match parser.parse_data_type()? { - DataType::Interval => parser.parse_interval(), + DataType::Interval { .. } => parser.parse_interval(), // PostgreSQL allows almost any identifier to be used as custom data type name, // and we support that in `parse_data_type()`. But unlike Postgres we don't // have a list of globally reserved keywords (since they vary across dialects), @@ -1450,10 +1547,11 @@ impl<'a> Parser<'a> { // an unary negation `NOT ('a' LIKE 'b')`. To solve this, we don't accept the // `type 'string'` syntax for the custom data types at all. DataType::Custom(..) => parser_err!("dummy", loc), - data_type => Ok(Expr::TypedString { + data_type => Ok(Expr::TypedString(TypedString { data_type, - value: parser.parse_value()?.value, - }), + value: parser.parse_value()?, + uses_odbc_syntax: false, + })), } })?; @@ -1562,8 +1660,7 @@ impl<'a> Parser<'a> { Token::QuestionPipe => UnaryOperator::QuestionPipe, _ => { return Err(ParserError::ParserError(format!( - "Unexpected token in unary operator parsing: {:?}", - tok + "Unexpected token in unary operator parsing: {tok:?}" ))) } }; @@ -1629,7 +1726,7 @@ impl<'a> Parser<'a> { _ => self.expected_at("an expression", next_token_index), }?; - if self.parse_keyword(Keyword::COLLATE) { + if !self.in_column_definition_state() && self.parse_keyword(Keyword::COLLATE) { Ok(Expr::Collate { expr: Box::new(expr), collation: self.parse_object_name(false)?, @@ -1640,11 +1737,11 @@ impl<'a> Parser<'a> { } fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result { - let value: Value = self.parse_value()?.value; - Ok(Expr::TypedString { + Ok(Expr::TypedString(TypedString { data_type: DataType::GeometricType(kind), - value, - }) + value: self.parse_value()?, + uses_odbc_syntax: false, + })) } /// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`. @@ -1941,6 +2038,50 @@ impl<'a> Parser<'a> { }) } + /// Tries to parse the body of an [ODBC escaping sequence] + /// i.e. without the enclosing braces + /// Currently implemented: + /// Scalar Function Calls + /// Date, Time, and Timestamp Literals + /// See + fn maybe_parse_odbc_body(&mut self) -> Result, ParserError> { + // Attempt 1: Try to parse it as a function. + if let Some(expr) = self.maybe_parse_odbc_fn_body()? { + return Ok(Some(expr)); + } + // Attempt 2: Try to parse it as a Date, Time or Timestamp Literal + self.maybe_parse_odbc_body_datetime() + } + + /// Tries to parse the body of an [ODBC Date, Time, and Timestamp Literals] call. + /// + /// ```sql + /// {d '2025-07-17'} + /// {t '14:12:01'} + /// {ts '2025-07-17 14:12:01'} + /// ``` + /// + /// [ODBC Date, Time, and Timestamp Literals]: + /// https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/date-time-and-timestamp-literals?view=sql-server-2017 + fn maybe_parse_odbc_body_datetime(&mut self) -> Result, ParserError> { + self.maybe_parse(|p| { + let token = p.next_token().clone(); + let word_string = token.token.to_string(); + let data_type = match word_string.as_str() { + "t" => DataType::Time(None, TimezoneInfo::None), + "d" => DataType::Date, + "ts" => DataType::Timestamp(None, TimezoneInfo::None), + _ => return p.expected("ODBC datetime keyword (t, d, or ts)", token), + }; + let value = p.parse_value()?; + Ok(Expr::TypedString(TypedString { + data_type, + value, + uses_odbc_syntax: true, + })) + }) + } + /// Tries to parse the body of an [ODBC function] call. /// i.e. without the enclosing braces /// @@ -2205,6 +2346,7 @@ impl<'a> Parser<'a> { } pub fn parse_case_expr(&mut self) -> Result { + let case_token = AttachedToken(self.get_current_token().clone()); let mut operand = None; if !self.parse_keyword(Keyword::WHEN) { operand = Some(Box::new(self.parse_expr()?)); @@ -2225,8 +2367,10 @@ impl<'a> Parser<'a> { } else { None }; - self.expect_keyword_is(Keyword::END)?; + let end_token = AttachedToken(self.expect_keyword(Keyword::END)?); Ok(Expr::Case { + case_token, + end_token, operand, conditions, else_result, @@ -2489,10 +2633,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let mut trim_where = None; if let Token::Word(word) = self.peek_token().token { - if [Keyword::BOTH, Keyword::LEADING, Keyword::TRAILING] - .iter() - .any(|d| word.keyword == *d) - { + if [Keyword::BOTH, Keyword::LEADING, Keyword::TRAILING].contains(&word.keyword) { trim_where = Some(self.parse_trim_where()?); } } @@ -2508,7 +2649,7 @@ impl<'a> Parser<'a> { trim_characters: None, }) } else if self.consume_token(&Token::Comma) - && dialect_of!(self is SnowflakeDialect | BigQueryDialect | GenericDialect) + && dialect_of!(self is DuckDbDialect | SnowflakeDialect | BigQueryDialect | GenericDialect) { let characters = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; @@ -2695,14 +2836,14 @@ impl<'a> Parser<'a> { fn parse_lbrace_expr(&mut self) -> Result { let token = self.expect_token(&Token::LBrace)?; - if let Some(fn_expr) = self.maybe_parse_odbc_fn_body()? { + if let Some(fn_expr) = self.maybe_parse_odbc_body()? { self.expect_token(&Token::RBrace)?; return Ok(fn_expr); } if self.dialect.supports_dictionary_syntax() { self.prev_token(); // Put back the '{' - return self.parse_duckdb_struct_literal(); + return self.parse_dictionary(); } self.expected("an expression", token) @@ -2965,7 +3106,6 @@ impl<'a> Parser<'a> { where F: FnMut(&mut Parser<'a>) -> Result<(StructField, MatchedTrailingBracket), ParserError>, { - let start_token = self.peek_token(); self.expect_keyword_is(Keyword::STRUCT)?; // Nothing to do if we have no type information. @@ -2978,16 +3118,10 @@ impl<'a> Parser<'a> { let trailing_bracket = loop { let (def, trailing_bracket) = elem_parser(self)?; field_defs.push(def); - if !self.consume_token(&Token::Comma) { + // The struct field definition is finished if it occurs `>>` or comma. + if trailing_bracket.0 || !self.consume_token(&Token::Comma) { break trailing_bracket; } - - // Angle brackets are balanced so we only expect the trailing `>>` after - // we've matched all field types for the current struct. - // e.g. this is invalid syntax `STRUCT>>, INT>(NULL)` - if trailing_bracket.0 { - return parser_err!("unmatched > in STRUCT definition", start_token.span.start); - } }; Ok(( @@ -3007,6 +3141,7 @@ impl<'a> Parser<'a> { Ok(StructField { field_name: Some(field_name), field_type, + options: None, }) }); self.expect_token(&Token::RParen)?; @@ -3040,10 +3175,12 @@ impl<'a> Parser<'a> { let (field_type, trailing_bracket) = self.parse_data_type_helper()?; + let options = self.maybe_parse_options(Keyword::OPTIONS)?; Ok(( StructField { field_name, field_type, + options, }, trailing_bracket, )) @@ -3075,7 +3212,7 @@ impl<'a> Parser<'a> { Ok(fields) } - /// DuckDB specific: Parse a duckdb [dictionary] + /// DuckDB and ClickHouse specific: Parse a duckdb [dictionary] or a clickhouse [map] setting /// /// Syntax: /// @@ -3084,18 +3221,18 @@ impl<'a> Parser<'a> { /// ``` /// /// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs - fn parse_duckdb_struct_literal(&mut self) -> Result { + /// [map]: https://clickhouse.com/docs/operations/settings/settings#additional_table_filters + fn parse_dictionary(&mut self) -> Result { self.expect_token(&Token::LBrace)?; - let fields = - self.parse_comma_separated0(Self::parse_duckdb_dictionary_field, Token::RBrace)?; + let fields = self.parse_comma_separated0(Self::parse_dictionary_field, Token::RBrace)?; self.expect_token(&Token::RBrace)?; Ok(Expr::Dictionary(fields)) } - /// Parse a field for a duckdb [dictionary] + /// Parse a field for a duckdb [dictionary] or a clickhouse [map] setting /// /// Syntax /// @@ -3104,7 +3241,8 @@ impl<'a> Parser<'a> { /// ``` /// /// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs - fn parse_duckdb_dictionary_field(&mut self) -> Result { + /// [map]: https://clickhouse.com/docs/operations/settings/settings#additional_table_filters + fn parse_dictionary_field(&mut self) -> Result { let key = self.parse_identifier()?; self.expect_token(&Token::Colon)?; @@ -3238,6 +3376,7 @@ impl<'a> Parser<'a> { self.advance_token(); let tok = self.get_current_token(); + debug!("infix: {tok:?}"); let tok_index = self.get_current_index(); let span = tok.span; let regular_binary_operator = match &tok.token { @@ -3414,10 +3553,18 @@ impl<'a> Parser<'a> { | BinaryOperator::LtEq | BinaryOperator::Eq | BinaryOperator::NotEq + | BinaryOperator::PGRegexMatch + | BinaryOperator::PGRegexIMatch + | BinaryOperator::PGRegexNotMatch + | BinaryOperator::PGRegexNotIMatch + | BinaryOperator::PGLikeMatch + | BinaryOperator::PGILikeMatch + | BinaryOperator::PGNotLikeMatch + | BinaryOperator::PGNotILikeMatch ) { return parser_err!( format!( - "Expected one of [=, >, <, =>, =<, !=] as comparison operator, found: {op}" + "Expected one of [=, >, <, =>, =<, !=, ~, ~*, !~, !~*, ~~, ~~*, !~~, !~~*] as comparison operator, found: {op}" ), span.start ); @@ -3498,6 +3645,11 @@ impl<'a> Parser<'a> { let negated = self.parse_keyword(Keyword::NOT); let regexp = self.parse_keyword(Keyword::REGEXP); let rlike = self.parse_keyword(Keyword::RLIKE); + let null = if !self.in_column_definition_state() { + self.parse_keyword(Keyword::NULL) + } else { + false + }; if regexp || rlike { Ok(Expr::RLike { negated, @@ -3507,6 +3659,8 @@ impl<'a> Parser<'a> { ), regexp, }) + } else if negated && null { + Ok(Expr::IsNotNull(Box::new(expr))) } else if self.parse_keyword(Keyword::IN) { self.parse_in(expr, negated) } else if self.parse_keyword(Keyword::BETWEEN) { @@ -3544,6 +3698,22 @@ impl<'a> Parser<'a> { self.expected("IN or BETWEEN after NOT", self.peek_token()) } } + Keyword::NOTNULL if dialect.supports_notnull_operator() => { + Ok(Expr::IsNotNull(Box::new(expr))) + } + Keyword::MEMBER => { + if self.parse_keyword(Keyword::OF) { + self.expect_token(&Token::LParen)?; + let array = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Ok(Expr::MemberOf(MemberOf { + value: Box::new(expr), + array: Box::new(array), + })) + } else { + self.expected("OF after MEMBER", self.peek_token()) + } + } // Can only happen if `get_next_precedence` got out of sync with this function _ => parser_err!( format!("No infix parser for token {:?}", tok.token), @@ -3577,9 +3747,9 @@ impl<'a> Parser<'a> { } /// Parse the `ESCAPE CHAR` portion of `LIKE`, `ILIKE`, and `SIMILAR TO` - pub fn parse_escape_char(&mut self) -> Result, ParserError> { + pub fn parse_escape_char(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::ESCAPE) { - Ok(Some(self.parse_literal_string()?)) + Ok(Some(self.parse_value()?.into())) } else { Ok(None) } @@ -3752,7 +3922,7 @@ impl<'a> Parser<'a> { }); } self.expect_token(&Token::LParen)?; - let in_op = match self.maybe_parse(|p| p.parse_query_body(p.dialect.prec_unknown()))? { + let in_op = match self.maybe_parse(|p| p.parse_query())? { Some(subquery) => Expr::InSubquery { expr: Box::new(expr), subquery, @@ -4075,6 +4245,18 @@ impl<'a> Parser<'a> { /// not be efficient as it does a loop on the tokens with `peek_nth_token` /// each time. pub fn parse_keyword_with_tokens(&mut self, expected: Keyword, tokens: &[Token]) -> bool { + self.keyword_with_tokens(expected, tokens, true) + } + + /// Peeks to see if the current token is the `expected` keyword followed by specified tokens + /// without consuming them. + /// + /// See [Self::parse_keyword_with_tokens] for details. + pub(crate) fn peek_keyword_with_tokens(&mut self, expected: Keyword, tokens: &[Token]) -> bool { + self.keyword_with_tokens(expected, tokens, false) + } + + fn keyword_with_tokens(&mut self, expected: Keyword, tokens: &[Token], consume: bool) -> bool { match &self.peek_token_ref().token { Token::Word(w) if expected == w.keyword => { for (idx, token) in tokens.iter().enumerate() { @@ -4082,10 +4264,13 @@ impl<'a> Parser<'a> { return false; } } - // consume all tokens - for _ in 0..(tokens.len() + 1) { - self.advance_token(); + + if consume { + for _ in 0..(tokens.len() + 1) { + self.advance_token(); + } } + true } _ => false, @@ -4284,11 +4469,7 @@ impl<'a> Parser<'a> { self.parse_comma_separated_with_trailing_commas( Parser::parse_table_and_joins, trailing_commas, - |kw, _parser| { - self.dialect - .get_reserved_keywords_for_table_factor() - .contains(kw) - }, + |kw, parser| !self.dialect.is_table_factor(kw, parser), ) } @@ -4448,11 +4629,16 @@ impl<'a> Parser<'a> { ) -> Result, ParserError> { let mut values = vec![]; loop { - if let Token::Word(w) = &self.peek_nth_token_ref(0).token { - if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) { - break; + match &self.peek_nth_token_ref(0).token { + Token::EOF => break, + Token::Word(w) => { + if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) { + break; + } } + _ => {} } + values.push(self.parse_statement()?); self.expect_token(&Token::SemiColon)?; } @@ -4555,14 +4741,18 @@ impl<'a> Parser<'a> { self.parse_create_external_table(or_replace) } else if self.parse_keyword(Keyword::FUNCTION) { self.parse_create_function(or_alter, or_replace, temporary) + } else if self.parse_keyword(Keyword::DOMAIN) { + self.parse_create_domain() } else if self.parse_keyword(Keyword::TRIGGER) { - self.parse_create_trigger(or_replace, false) + self.parse_create_trigger(or_alter, or_replace, false) } else if self.parse_keywords(&[Keyword::CONSTRAINT, Keyword::TRIGGER]) { - self.parse_create_trigger(or_replace, true) + self.parse_create_trigger(or_alter, or_replace, true) } else if self.parse_keyword(Keyword::MACRO) { self.parse_create_macro(or_replace, temporary) } else if self.parse_keyword(Keyword::SECRET) { self.parse_create_secret(or_replace, temporary, persistent) + } else if self.parse_keyword(Keyword::USER) { + self.parse_create_user(or_replace) } else if or_replace { self.expected( "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after CREATE OR REPLACE", @@ -4590,11 +4780,39 @@ impl<'a> Parser<'a> { self.parse_create_procedure(or_alter) } else if self.parse_keyword(Keyword::CONNECTOR) { self.parse_create_connector() + } else if self.parse_keyword(Keyword::SERVER) { + self.parse_pg_create_server() } else { self.expected("an object type after CREATE", self.peek_token()) } } + fn parse_create_user(&mut self, or_replace: bool) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_identifier()?; + let options = self.parse_key_value_options(false, &[Keyword::WITH, Keyword::TAG])?; + let with_tags = self.parse_keyword(Keyword::WITH); + let tags = if self.parse_keyword(Keyword::TAG) { + self.parse_key_value_options(true, &[])? + } else { + vec![] + }; + Ok(Statement::CreateUser(CreateUser { + or_replace, + if_not_exists, + name, + options: KeyValueOptions { + options, + delimiter: KeyValueOptionsDelimiter::Space, + }, + with_tags, + tags: KeyValueOptions { + options: tags, + delimiter: KeyValueOptionsDelimiter::Comma, + }, + })) + } + /// See [DuckDB Docs](https://duckdb.org/docs/sql/statements/create_secret.html) for more details. pub fn parse_create_secret( &mut self, @@ -4786,17 +5004,31 @@ impl<'a> Parser<'a> { None }; + let with = if self.peek_keyword(Keyword::WITH) { + Some(self.parse_options(Keyword::WITH)?) + } else { + None + }; + let options = if self.peek_keyword(Keyword::OPTIONS) { Some(self.parse_options(Keyword::OPTIONS)?) } else { None }; + let clone = if self.parse_keyword(Keyword::CLONE) { + Some(self.parse_object_name(false)?) + } else { + None + }; + Ok(Statement::CreateSchema { schema_name, if_not_exists, + with, options, default_collate_spec, + clone, }) } @@ -4831,11 +5063,33 @@ impl<'a> Parser<'a> { _ => break, } } + let clone = if self.parse_keyword(Keyword::CLONE) { + Some(self.parse_object_name(false)?) + } else { + None + }; + Ok(Statement::CreateDatabase { db_name, if_not_exists: ine, location, managed_location, + or_replace: false, + transient: false, + clone, + data_retention_time_in_days: None, + max_data_extension_time_in_days: None, + external_volume: None, + catalog: None, + replace_invalid_characters: None, + default_ddl_collation: None, + storage_serialization_policy: None, + comment: None, + catalog_sync: None, + catalog_sync_namespace_mode: None, + catalog_sync_namespace_flatten_delimiter: None, + with_tags: None, + with_contacts: None, }) } @@ -5131,19 +5385,62 @@ impl<'a> Parser<'a> { let (name, args) = self.parse_create_function_name_and_params()?; self.expect_keyword(Keyword::RETURNS)?; - let return_type = Some(self.parse_data_type()?); - self.expect_keyword_is(Keyword::AS)?; + let return_table = self.maybe_parse(|p| { + let return_table_name = p.parse_identifier()?; - let begin_token = self.expect_keyword(Keyword::BEGIN)?; - let statements = self.parse_statement_list(&[Keyword::END])?; - let end_token = self.expect_keyword(Keyword::END)?; + p.expect_keyword_is(Keyword::TABLE)?; + p.prev_token(); - let function_body = Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { - begin_token: AttachedToken(begin_token), - statements, - end_token: AttachedToken(end_token), - })); + let table_column_defs = match p.parse_data_type()? { + DataType::Table(Some(table_column_defs)) if !table_column_defs.is_empty() => { + table_column_defs + } + _ => parser_err!( + "Expected table column definitions after TABLE keyword", + p.peek_token().span.start + )?, + }; + + Ok(DataType::NamedTable { + name: ObjectName(vec![ObjectNamePart::Identifier(return_table_name)]), + columns: table_column_defs, + }) + })?; + + let return_type = if return_table.is_some() { + return_table + } else { + Some(self.parse_data_type()?) + }; + + let _ = self.parse_keyword(Keyword::AS); + + let function_body = if self.peek_keyword(Keyword::BEGIN) { + let begin_token = self.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(&[Keyword::END])?; + let end_token = self.expect_keyword(Keyword::END)?; + + Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + })) + } else if self.parse_keyword(Keyword::RETURN) { + if self.peek_token() == Token::LParen { + Some(CreateFunctionBody::AsReturnExpr(self.parse_expr()?)) + } else if self.peek_keyword(Keyword::SELECT) { + let select = self.parse_select()?; + Some(CreateFunctionBody::AsReturnSelect(select)) + } else { + parser_err!( + "Expected a subquery (or bare SELECT statement) after RETURN", + self.peek_token().span.start + )? + } + } else { + parser_err!("Unparsable function body", self.peek_token().span.start)? + }; Ok(Statement::CreateFunction(CreateFunction { or_alter, @@ -5173,11 +5470,17 @@ impl<'a> Parser<'a> { |parser: &mut Parser| -> Result { let name = parser.parse_identifier()?; let data_type = parser.parse_data_type()?; + let default_expr = if parser.consume_token(&Token::Eq) { + Some(parser.parse_expr()?) + } else { + None + }; + Ok(OperateFunctionArg { mode: None, name: Some(name), data_type, - default_expr: None, + default_expr, }) }; self.expect_token(&Token::LParen)?; @@ -5200,12 +5503,21 @@ impl<'a> Parser<'a> { // parse: [ argname ] argtype let mut name = None; let mut data_type = self.parse_data_type()?; - if let DataType::Custom(n, _) = &data_type { - // the first token is actually a name - match n.0[0].clone() { - ObjectNamePart::Identifier(ident) => name = Some(ident), + + // To check whether the first token is a name or a type, we need to + // peek the next token, which if it is another type keyword, then the + // first token is a name and not a type in itself. + let data_type_idx = self.get_current_index(); + if let Some(next_data_type) = self.maybe_parse(|parser| parser.parse_data_type())? { + let token = self.token_at(data_type_idx); + + // We ensure that the token is a `Word` token, and not other special tokens. + if !matches!(token.token, Token::Word(_)) { + return self.expected("a name or type", token.clone()); } - data_type = self.parse_data_type()?; + + name = Some(Ident::new(token.to_string())); + data_type = next_data_type; } let default_expr = if self.parse_keyword(Keyword::DEFAULT) || self.consume_token(&Token::Eq) @@ -5256,10 +5568,11 @@ impl<'a> Parser<'a> { pub fn parse_create_trigger( &mut self, + or_alter: bool, or_replace: bool, is_constraint: bool, ) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) { + if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) { self.prev_token(); return self.expected("an object type after CREATE", self.peek_token()); } @@ -5300,15 +5613,21 @@ impl<'a> Parser<'a> { .then(|| self.parse_expr()) .transpose()?; - self.expect_keyword_is(Keyword::EXECUTE)?; - - let exec_body = self.parse_trigger_exec_body()?; + let mut exec_body = None; + let mut statements = None; + if self.parse_keyword(Keyword::EXECUTE) { + exec_body = Some(self.parse_trigger_exec_body()?); + } else { + statements = Some(self.parse_conditional_statements(&[Keyword::END])?); + } Ok(Statement::CreateTrigger { + or_alter, or_replace, is_constraint, name, period, + period_before_table: true, events, table_name, referenced_table_name, @@ -5317,6 +5636,8 @@ impl<'a> Parser<'a> { include_each, condition, exec_body, + statements_as: false, + statements, characteristics, }) } @@ -5324,10 +5645,12 @@ impl<'a> Parser<'a> { pub fn parse_trigger_period(&mut self) -> Result { Ok( match self.expect_one_of_keywords(&[ + Keyword::FOR, Keyword::BEFORE, Keyword::AFTER, Keyword::INSTEAD, ])? { + Keyword::FOR => TriggerPeriod::For, Keyword::BEFORE => TriggerPeriod::Before, Keyword::AFTER => TriggerPeriod::After, Keyword::INSTEAD => self @@ -5466,12 +5789,17 @@ impl<'a> Parser<'a> { }; let location = hive_formats.location.clone(); let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; + let table_options = if !table_properties.is_empty() { + CreateTableOptions::TableProperties(table_properties) + } else { + CreateTableOptions::None + }; Ok(CreateTableBuilder::new(table_name) .columns(columns) .constraints(constraints) .hive_distribution(hive_distribution) .hive_formats(Some(hive_formats)) - .table_properties(table_properties) + .table_options(table_options) .or_replace(or_replace) .if_not_exists(if_not_exists) .external(true) @@ -5497,6 +5825,14 @@ impl<'a> Parser<'a> { } } + fn parse_analyze_format_kind(&mut self) -> Result { + if self.consume_token(&Token::Eq) { + Ok(AnalyzeFormatKind::Assignment(self.parse_analyze_format()?)) + } else { + Ok(AnalyzeFormatKind::Keyword(self.parse_analyze_format()?)) + } + } + pub fn parse_analyze_format(&mut self) -> Result { let next_token = self.next_token(); match &next_token.token { @@ -5519,12 +5855,17 @@ impl<'a> Parser<'a> { ) -> Result { let materialized = self.parse_keyword(Keyword::MATERIALIZED); self.expect_keyword_is(Keyword::VIEW)?; - let if_not_exists = dialect_of!(self is BigQueryDialect|SQLiteDialect|GenericDialect) + let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); + // Tries to parse IF NOT EXISTS either before name or after name + // Name before IF NOT EXISTS is supported by snowflake but undocumented + let if_not_exists_first = + self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(allow_unquoted_hyphen)?; + let name_before_not_exists = !if_not_exists_first && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let if_not_exists = if_not_exists_first || name_before_not_exists; // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. - let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); - let name = self.parse_object_name(allow_unquoted_hyphen)?; let columns = self.parse_view_columns()?; let mut options = CreateTableOptions::None; let with_options = self.parse_options(Keyword::WITH)?; @@ -5591,6 +5932,7 @@ impl<'a> Parser<'a> { temporary, to, params: create_view_params, + name_before_not_exists, }) } @@ -5894,6 +6236,35 @@ impl<'a> Parser<'a> { Ok(owner) } + /// Parses a [Statement::CreateDomain] statement. + fn parse_create_domain(&mut self) -> Result { + let name = self.parse_object_name(false)?; + self.expect_keyword_is(Keyword::AS)?; + let data_type = self.parse_data_type()?; + let collation = if self.parse_keyword(Keyword::COLLATE) { + Some(self.parse_identifier()?) + } else { + None + }; + let default = if self.parse_keyword(Keyword::DEFAULT) { + Some(self.parse_expr()?) + } else { + None + }; + let mut constraints = Vec::new(); + while let Some(constraint) = self.parse_optional_table_constraint()? { + constraints.push(constraint); + } + + Ok(Statement::CreateDomain(CreateDomain { + name, + data_type, + collation, + default, + constraints, + })) + } + /// ```sql /// CREATE POLICY name ON table_name [ AS { PERMISSIVE | RESTRICTIVE } ] /// [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ] @@ -6045,12 +6416,18 @@ impl<'a> Parser<'a> { ObjectType::Stage } else if self.parse_keyword(Keyword::TYPE) { ObjectType::Type + } else if self.parse_keyword(Keyword::USER) { + ObjectType::User + } else if self.parse_keyword(Keyword::STREAM) { + ObjectType::Stream } else if self.parse_keyword(Keyword::FUNCTION) { return self.parse_drop_function(); } else if self.parse_keyword(Keyword::POLICY) { return self.parse_drop_policy(); } else if self.parse_keyword(Keyword::CONNECTOR) { return self.parse_drop_connector(); + } else if self.parse_keyword(Keyword::DOMAIN) { + return self.parse_drop_domain(); } else if self.parse_keyword(Keyword::PROCEDURE) { return self.parse_drop_procedure(); } else if self.parse_keyword(Keyword::SECRET) { @@ -6061,7 +6438,7 @@ impl<'a> Parser<'a> { return self.parse_drop_extension(); } else { return self.expected( - "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, or MATERIALIZED VIEW after DROP", + "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", self.peek_token(), ); }; @@ -6083,6 +6460,11 @@ impl<'a> Parser<'a> { loc ); } + let table = if self.parse_keyword(Keyword::ON) { + Some(self.parse_object_name(false)?) + } else { + None + }; Ok(Statement::Drop { object_type, if_exists, @@ -6091,6 +6473,7 @@ impl<'a> Parser<'a> { restrict, purge, temporary, + table, }) } @@ -6146,6 +6529,20 @@ impl<'a> Parser<'a> { Ok(Statement::DropConnector { if_exists, name }) } + /// ```sql + /// DROP DOMAIN [ IF EXISTS ] name [ CASCADE | RESTRICT ] + /// ``` + fn parse_drop_domain(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + let drop_behavior = self.parse_optional_drop_behavior(); + Ok(Statement::DropDomain(DropDomain { + if_exists, + name, + drop_behavior, + })) + } + /// ```sql /// DROP PROCEDURE [ IF EXISTS ] name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] [, ...] /// [ CASCADE | RESTRICT ] @@ -6166,7 +6563,7 @@ impl<'a> Parser<'a> { let args = if self.consume_token(&Token::LParen) { if self.consume_token(&Token::RParen) { - None + Some(vec![]) } else { let args = self.parse_comma_separated(Parser::parse_function_arg)?; self.expect_token(&Token::RParen)?; @@ -6609,7 +7006,15 @@ impl<'a> Parser<'a> { } }; - self.expect_one_of_keywords(&[Keyword::FROM, Keyword::IN])?; + let position = if self.peek_keyword(Keyword::FROM) { + self.expect_keyword(Keyword::FROM)?; + FetchPosition::From + } else if self.peek_keyword(Keyword::IN) { + self.expect_keyword(Keyword::IN)?; + FetchPosition::In + } else { + return parser_err!("Expected FROM or IN", self.peek_token().span.start); + }; let name = self.parse_identifier()?; @@ -6622,6 +7027,7 @@ impl<'a> Parser<'a> { Ok(Statement::Fetch { name, direction, + position, into, }) } @@ -6661,9 +7067,7 @@ impl<'a> Parser<'a> { None }; - self.expect_token(&Token::LParen)?; - let columns = self.parse_comma_separated(Parser::parse_create_index_expr)?; - self.expect_token(&Token::RParen)?; + let columns = self.parse_parenthesized_index_column_list()?; let include = if self.parse_keyword(Keyword::INCLUDE) { self.expect_token(&Token::LParen)?; @@ -6699,6 +7103,22 @@ impl<'a> Parser<'a> { None }; + // MySQL options (including the modern style of `USING` after the column list instead of + // before, which is deprecated) shouldn't conflict with other preceding options (e.g. `WITH + // PARSER` won't be caught by the above `WITH` clause parsing because MySQL doesn't set that + // support flag). This is probably invalid syntax for other dialects, but it is simpler to + // parse it anyway (as we do inside `ALTER TABLE` and `CREATE TABLE` parsing). + let index_options = self.parse_index_options()?; + + // MySQL allows `ALGORITHM` and `LOCK` options. Unlike in `ALTER TABLE`, they need not be comma separated. + let mut alter_options = Vec::new(); + while self + .peek_one_of_keywords(&[Keyword::ALGORITHM, Keyword::LOCK]) + .is_some() + { + alter_options.push(self.parse_alter_table_operation()?) + } + Ok(Statement::CreateIndex(CreateIndex { name: index_name, table_name, @@ -6711,6 +7131,8 @@ impl<'a> Parser<'a> { nulls_distinct, with, predicate, + index_options, + alter_options, })) } @@ -6944,11 +7366,7 @@ impl<'a> Parser<'a> { // Clickhouse has `ON CLUSTER 'cluster'` syntax for DDLs let on_cluster = self.parse_optional_on_cluster()?; - let like = if self.parse_keyword(Keyword::LIKE) || self.parse_keyword(Keyword::ILIKE) { - self.parse_object_name(allow_unquoted_hyphen).ok() - } else { - None - }; + let like = self.maybe_parse_create_table_like(allow_unquoted_hyphen)?; let clone = if self.parse_keyword(Keyword::CLONE) { self.parse_object_name(allow_unquoted_hyphen).ok() @@ -6958,17 +7376,16 @@ impl<'a> Parser<'a> { // parse optional column list (schema) let (columns, constraints) = self.parse_columns()?; - let mut comment = if dialect_of!(self is HiveDialect) - && self.parse_keyword(Keyword::COMMENT) - { - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(str) => Some(CommentDef::AfterColumnDefsWithoutEq(str)), - _ => self.expected("comment", next_token)?, - } - } else { - None - }; + let comment_after_column_def = + if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { + let next_token = self.next_token(); + match next_token.token { + Token::SingleQuotedString(str) => Some(CommentDef::WithoutEq(str)), + _ => self.expected("comment", next_token)?, + } + } else { + None + }; // SQLite supports `WITHOUT ROWID` at the end of `CREATE TABLE` let without_rowid = self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]); @@ -6976,39 +7393,8 @@ impl<'a> Parser<'a> { let hive_distribution = self.parse_hive_distribution()?; let clustered_by = self.parse_optional_clustered_by()?; let hive_formats = self.parse_hive_formats()?; - // PostgreSQL supports `WITH ( options )`, before `AS` - let with_options = self.parse_options(Keyword::WITH)?; - let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; - let engine = if self.parse_keyword(Keyword::ENGINE) { - self.expect_token(&Token::Eq)?; - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => { - let name = w.value; - let parameters = if self.peek_token() == Token::LParen { - Some(self.parse_parenthesized_identifiers()?) - } else { - None - }; - Some(TableEngine { name, parameters }) - } - _ => self.expected("identifier", next_token)?, - } - } else { - None - }; - - let auto_increment_offset = if self.parse_keyword(Keyword::AUTO_INCREMENT) { - let _ = self.consume_token(&Token::Eq); - let next_token = self.next_token(); - match next_token.token { - Token::Number(s, _) => Some(Self::parse::(s, next_token.span.start)?), - _ => self.expected("literal int", next_token)?, - } - } else { - None - }; + let create_table_config = self.parse_optional_create_table_config()?; // ClickHouse supports `PRIMARY KEY`, before `ORDER BY` // https://clickhouse.com/docs/en/sql-reference/statements/create/table#primary-key @@ -7036,30 +7422,6 @@ impl<'a> Parser<'a> { None }; - let create_table_config = self.parse_optional_create_table_config()?; - - let default_charset = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARSET]) { - self.expect_token(&Token::Eq)?; - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => Some(w.value), - _ => self.expected("identifier", next_token)?, - } - } else { - None - }; - - let collation = if self.parse_keywords(&[Keyword::COLLATE]) { - self.expect_token(&Token::Eq)?; - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => Some(w.value), - _ => self.expected("identifier", next_token)?, - } - } else { - None - }; - let on_commit = if self.parse_keywords(&[Keyword::ON, Keyword::COMMIT]) { Some(self.parse_create_table_on_commit()?) } else { @@ -7068,13 +7430,6 @@ impl<'a> Parser<'a> { let strict = self.parse_keyword(Keyword::STRICT); - // Excludes Hive dialect here since it has been handled after table column definitions. - if !dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { - // rewind the COMMENT keyword - self.prev_token(); - comment = self.parse_optional_inline_comment()? - }; - // Parse optional `AS ( query )` let query = if self.parse_keyword(Keyword::AS) { Some(self.parse_query()?) @@ -7091,8 +7446,6 @@ impl<'a> Parser<'a> { .temporary(temporary) .columns(columns) .constraints(constraints) - .with_options(with_options) - .table_properties(table_properties) .or_replace(or_replace) .if_not_exists(if_not_exists) .transient(transient) @@ -7103,24 +7456,58 @@ impl<'a> Parser<'a> { .without_rowid(without_rowid) .like(like) .clone_clause(clone) - .engine(engine) - .comment(comment) - .auto_increment_offset(auto_increment_offset) + .comment_after_column_def(comment_after_column_def) .order_by(order_by) - .default_charset(default_charset) - .collation(collation) .on_commit(on_commit) .on_cluster(on_cluster) .clustered_by(clustered_by) .partition_by(create_table_config.partition_by) .cluster_by(create_table_config.cluster_by) - .options(create_table_config.options) .inherits(create_table_config.inherits) + .table_options(create_table_config.table_options) .primary_key(primary_key) .strict(strict) .build()) } + fn maybe_parse_create_table_like( + &mut self, + allow_unquoted_hyphen: bool, + ) -> Result, ParserError> { + let like = if self.dialect.supports_create_table_like_parenthesized() + && self.consume_token(&Token::LParen) + { + if self.parse_keyword(Keyword::LIKE) { + let name = self.parse_object_name(allow_unquoted_hyphen)?; + let defaults = if self.parse_keywords(&[Keyword::INCLUDING, Keyword::DEFAULTS]) { + Some(CreateTableLikeDefaults::Including) + } else if self.parse_keywords(&[Keyword::EXCLUDING, Keyword::DEFAULTS]) { + Some(CreateTableLikeDefaults::Excluding) + } else { + None + }; + self.expect_token(&Token::RParen)?; + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name, + defaults, + })) + } else { + // Rollback the '(' it's probably the columns list + self.prev_token(); + None + } + } else if self.parse_keyword(Keyword::LIKE) || self.parse_keyword(Keyword::ILIKE) { + let name = self.parse_object_name(allow_unquoted_hyphen)?; + Some(CreateTableLikeKind::Plain(CreateTableLike { + name, + defaults: None, + })) + } else { + None + }; + Ok(like) + } + pub(crate) fn parse_create_table_on_commit(&mut self) -> Result { if self.parse_keywords(&[Keyword::DELETE, Keyword::ROWS]) { Ok(OnCommit::DeleteRows) @@ -7139,17 +7526,29 @@ impl<'a> Parser<'a> { /// Parse configuration like inheritance, partitioning, clustering information during the table creation. /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_2) - /// [PostgreSQL Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) - /// [PostgreSQL Inheritance](https://www.postgresql.org/docs/current/ddl-inherit.html) + /// [PostgreSQL](https://www.postgresql.org/docs/current/ddl-partitioning.html) + /// [MySql](https://dev.mysql.com/doc/refman/8.4/en/create-table.html) fn parse_optional_create_table_config( &mut self, ) -> Result { + let mut table_options = CreateTableOptions::None; + let inherits = if self.parse_keyword(Keyword::INHERITS) { Some(self.parse_parenthesized_qualified_column_list(IsOptional::Mandatory, false)?) } else { None }; + // PostgreSQL supports `WITH ( options )`, before `AS` + let with_options = self.parse_options(Keyword::WITH)?; + if !with_options.is_empty() { + table_options = CreateTableOptions::With(with_options) + } + + let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; + if !table_properties.is_empty() { + table_options = CreateTableOptions::TableProperties(table_properties); + } let partition_by = if dialect_of!(self is BigQueryDialect | PostgreSqlDialect | GenericDialect) && self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { @@ -7159,29 +7558,242 @@ impl<'a> Parser<'a> { }; let mut cluster_by = None; - let mut options = None; if dialect_of!(self is BigQueryDialect | GenericDialect) { if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { cluster_by = Some(WrappedCollection::NoWrapping( - self.parse_comma_separated(|p| p.parse_identifier())?, + self.parse_comma_separated(|p| p.parse_expr())?, )); }; if let Token::Word(word) = self.peek_token().token { if word.keyword == Keyword::OPTIONS { - options = Some(self.parse_options(Keyword::OPTIONS)?); + table_options = + CreateTableOptions::Options(self.parse_options(Keyword::OPTIONS)?) } }; } + if !dialect_of!(self is HiveDialect) && table_options == CreateTableOptions::None { + let plain_options = self.parse_plain_options()?; + if !plain_options.is_empty() { + table_options = CreateTableOptions::Plain(plain_options) + } + }; + Ok(CreateTableConfiguration { partition_by, cluster_by, - options, inherits, + table_options, }) } + fn parse_plain_option(&mut self) -> Result, ParserError> { + // Single parameter option + // + if self.parse_keywords(&[Keyword::START, Keyword::TRANSACTION]) { + return Ok(Some(SqlOption::Ident(Ident::new("START TRANSACTION")))); + } + + // Custom option + // + if self.parse_keywords(&[Keyword::COMMENT]) { + let has_eq = self.consume_token(&Token::Eq); + let value = self.next_token(); + + let comment = match (has_eq, value.token) { + (true, Token::SingleQuotedString(s)) => { + Ok(Some(SqlOption::Comment(CommentDef::WithEq(s)))) + } + (false, Token::SingleQuotedString(s)) => { + Ok(Some(SqlOption::Comment(CommentDef::WithoutEq(s)))) + } + (_, token) => { + self.expected("Token::SingleQuotedString", TokenWithSpan::wrap(token)) + } + }; + return comment; + } + + // + // + if self.parse_keywords(&[Keyword::ENGINE]) { + let _ = self.consume_token(&Token::Eq); + let value = self.next_token(); + + let engine = match value.token { + Token::Word(w) => { + let parameters = if self.peek_token() == Token::LParen { + self.parse_parenthesized_identifiers()? + } else { + vec![] + }; + + Ok(Some(SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new(w.value)), + values: parameters, + }, + ))) + } + _ => { + return self.expected("Token::Word", value)?; + } + }; + + return engine; + } + + // + if self.parse_keywords(&[Keyword::TABLESPACE]) { + let _ = self.consume_token(&Token::Eq); + let value = self.next_token(); + + let tablespace = match value.token { + Token::Word(Word { value: name, .. }) | Token::SingleQuotedString(name) => { + let storage = match self.parse_keyword(Keyword::STORAGE) { + true => { + let _ = self.consume_token(&Token::Eq); + let storage_token = self.next_token(); + match &storage_token.token { + Token::Word(w) => match w.value.to_uppercase().as_str() { + "DISK" => Some(StorageType::Disk), + "MEMORY" => Some(StorageType::Memory), + _ => self + .expected("Storage type (DISK or MEMORY)", storage_token)?, + }, + _ => self.expected("Token::Word", storage_token)?, + } + } + false => None, + }; + + Ok(Some(SqlOption::TableSpace(TablespaceOption { + name, + storage, + }))) + } + _ => { + return self.expected("Token::Word", value)?; + } + }; + + return tablespace; + } + + // + if self.parse_keyword(Keyword::UNION) { + let _ = self.consume_token(&Token::Eq); + let value = self.next_token(); + + match value.token { + Token::LParen => { + let tables: Vec = + self.parse_comma_separated0(Parser::parse_identifier, Token::RParen)?; + self.expect_token(&Token::RParen)?; + + return Ok(Some(SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("UNION"), + name: None, + values: tables, + }, + ))); + } + _ => { + return self.expected("Token::LParen", value)?; + } + } + } + + // Key/Value parameter option + let key = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARSET]) { + Ident::new("DEFAULT CHARSET") + } else if self.parse_keyword(Keyword::CHARSET) { + Ident::new("CHARSET") + } else if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARACTER, Keyword::SET]) { + Ident::new("DEFAULT CHARACTER SET") + } else if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) { + Ident::new("CHARACTER SET") + } else if self.parse_keywords(&[Keyword::DEFAULT, Keyword::COLLATE]) { + Ident::new("DEFAULT COLLATE") + } else if self.parse_keyword(Keyword::COLLATE) { + Ident::new("COLLATE") + } else if self.parse_keywords(&[Keyword::DATA, Keyword::DIRECTORY]) { + Ident::new("DATA DIRECTORY") + } else if self.parse_keywords(&[Keyword::INDEX, Keyword::DIRECTORY]) { + Ident::new("INDEX DIRECTORY") + } else if self.parse_keyword(Keyword::KEY_BLOCK_SIZE) { + Ident::new("KEY_BLOCK_SIZE") + } else if self.parse_keyword(Keyword::ROW_FORMAT) { + Ident::new("ROW_FORMAT") + } else if self.parse_keyword(Keyword::PACK_KEYS) { + Ident::new("PACK_KEYS") + } else if self.parse_keyword(Keyword::STATS_AUTO_RECALC) { + Ident::new("STATS_AUTO_RECALC") + } else if self.parse_keyword(Keyword::STATS_PERSISTENT) { + Ident::new("STATS_PERSISTENT") + } else if self.parse_keyword(Keyword::STATS_SAMPLE_PAGES) { + Ident::new("STATS_SAMPLE_PAGES") + } else if self.parse_keyword(Keyword::DELAY_KEY_WRITE) { + Ident::new("DELAY_KEY_WRITE") + } else if self.parse_keyword(Keyword::COMPRESSION) { + Ident::new("COMPRESSION") + } else if self.parse_keyword(Keyword::ENCRYPTION) { + Ident::new("ENCRYPTION") + } else if self.parse_keyword(Keyword::MAX_ROWS) { + Ident::new("MAX_ROWS") + } else if self.parse_keyword(Keyword::MIN_ROWS) { + Ident::new("MIN_ROWS") + } else if self.parse_keyword(Keyword::AUTOEXTEND_SIZE) { + Ident::new("AUTOEXTEND_SIZE") + } else if self.parse_keyword(Keyword::AVG_ROW_LENGTH) { + Ident::new("AVG_ROW_LENGTH") + } else if self.parse_keyword(Keyword::CHECKSUM) { + Ident::new("CHECKSUM") + } else if self.parse_keyword(Keyword::CONNECTION) { + Ident::new("CONNECTION") + } else if self.parse_keyword(Keyword::ENGINE_ATTRIBUTE) { + Ident::new("ENGINE_ATTRIBUTE") + } else if self.parse_keyword(Keyword::PASSWORD) { + Ident::new("PASSWORD") + } else if self.parse_keyword(Keyword::SECONDARY_ENGINE_ATTRIBUTE) { + Ident::new("SECONDARY_ENGINE_ATTRIBUTE") + } else if self.parse_keyword(Keyword::INSERT_METHOD) { + Ident::new("INSERT_METHOD") + } else if self.parse_keyword(Keyword::AUTO_INCREMENT) { + Ident::new("AUTO_INCREMENT") + } else { + return Ok(None); + }; + + let _ = self.consume_token(&Token::Eq); + + let value = match self + .maybe_parse(|parser| parser.parse_value())? + .map(Expr::Value) + { + Some(expr) => expr, + None => Expr::Identifier(self.parse_identifier()?), + }; + + Ok(Some(SqlOption::KeyValue { key, value })) + } + + pub fn parse_plain_options(&mut self) -> Result, ParserError> { + let mut options = Vec::new(); + + while let Some(option) = self.parse_plain_option()? { + options.push(option); + // Some dialects support comma-separated options; it shouldn't introduce ambiguity to + // consume it for all dialects. + let _ = self.consume_token(&Token::Comma); + } + + Ok(options) + } + pub fn parse_optional_inline_comment(&mut self) -> Result, ParserError> { let comment = if self.parse_keyword(Keyword::COMMENT) { let has_eq = self.consume_token(&Token::Eq); @@ -7266,9 +7878,22 @@ impl<'a> Parser<'a> { } pub fn parse_procedure_param(&mut self) -> Result { + let mode = if self.parse_keyword(Keyword::IN) { + Some(ArgMode::In) + } else if self.parse_keyword(Keyword::OUT) { + Some(ArgMode::Out) + } else if self.parse_keyword(Keyword::INOUT) { + Some(ArgMode::InOut) + } else { + None + }; let name = self.parse_identifier()?; let data_type = self.parse_data_type()?; - Ok(ProcedureParam { name, data_type }) + Ok(ProcedureParam { + name, + data_type, + mode, + }) } pub fn parse_column_def(&mut self) -> Result { @@ -7331,6 +7956,15 @@ impl<'a> Parser<'a> { return option; } + self.with_state( + ColumnDefinition, + |parser| -> Result, ParserError> { + parser.parse_optional_column_option_inner() + }, + ) + } + + fn parse_optional_column_option_inner(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) { Ok(Some(ColumnOption::CharacterSet( self.parse_object_name(false)?, @@ -7346,15 +7980,19 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::NULL) { Ok(Some(ColumnOption::Null)) } else if self.parse_keyword(Keyword::DEFAULT) { - Ok(Some(ColumnOption::Default(self.parse_expr()?))) + Ok(Some(ColumnOption::Default( + self.parse_column_option_expr()?, + ))) } else if dialect_of!(self is ClickHouseDialect| GenericDialect) && self.parse_keyword(Keyword::MATERIALIZED) { - Ok(Some(ColumnOption::Materialized(self.parse_expr()?))) + Ok(Some(ColumnOption::Materialized( + self.parse_column_option_expr()?, + ))) } else if dialect_of!(self is ClickHouseDialect| GenericDialect) && self.parse_keyword(Keyword::ALIAS) { - Ok(Some(ColumnOption::Alias(self.parse_expr()?))) + Ok(Some(ColumnOption::Alias(self.parse_column_option_expr()?))) } else if dialect_of!(self is ClickHouseDialect| GenericDialect) && self.parse_keyword(Keyword::EPHEMERAL) { @@ -7363,7 +8001,9 @@ impl<'a> Parser<'a> { if matches!(self.peek_token().token, Token::Comma | Token::RParen) { Ok(Some(ColumnOption::Ephemeral(None))) } else { - Ok(Some(ColumnOption::Ephemeral(Some(self.parse_expr()?)))) + Ok(Some(ColumnOption::Ephemeral(Some( + self.parse_column_option_expr()?, + )))) } } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) { let characteristics = self.parse_constraint_characteristics()?; @@ -7406,7 +8046,8 @@ impl<'a> Parser<'a> { })) } else if self.parse_keyword(Keyword::CHECK) { self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; + // since `CHECK` requires parentheses, we can parse the inner expression in ParserState::Normal + let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_expr())?; self.expect_token(&Token::RParen)?; Ok(Some(ColumnOption::Check(expr))) } else if self.parse_keyword(Keyword::AUTO_INCREMENT) @@ -7440,7 +8081,7 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::ON, Keyword::UPDATE]) && dialect_of!(self is MySqlDialect | GenericDialect) { - let expr = self.parse_expr()?; + let expr = self.parse_column_option_expr()?; Ok(Some(ColumnOption::OnUpdate(expr))) } else if self.parse_keyword(Keyword::GENERATED) { self.parse_optional_column_option_generated() @@ -7455,6 +8096,12 @@ impl<'a> Parser<'a> { && dialect_of!(self is MySqlDialect | SQLiteDialect | DuckDbDialect | GenericDialect) { self.parse_optional_column_option_as() + } else if self.parse_keyword(Keyword::SRID) + && dialect_of!(self is MySqlDialect | GenericDialect) + { + Ok(Some(ColumnOption::Srid(Box::new( + self.parse_column_option_expr()?, + )))) } else if self.parse_keyword(Keyword::IDENTITY) && dialect_of!(self is MsSqlDialect | GenericDialect) { @@ -7494,8 +8141,33 @@ impl<'a> Parser<'a> { } } + /// When parsing some column option expressions we need to revert to [ParserState::Normal] since + /// `NOT NULL` is allowed as an alias for `IS NOT NULL`. + /// In those cases we use this helper instead of calling [Parser::parse_expr] directly. + /// + /// For example, consider these `CREATE TABLE` statements: + /// ```sql + /// CREATE TABLE foo (abc BOOL DEFAULT (42 NOT NULL) NOT NULL); + /// ``` + /// vs + /// ```sql + /// CREATE TABLE foo (abc BOOL NOT NULL); + /// ``` + /// + /// In the first we should parse the inner portion of `(42 NOT NULL)` as [Expr::IsNotNull], + /// whereas is both statements that trailing `NOT NULL` should only be parsed as a + /// [ColumnOption::NotNull]. + fn parse_column_option_expr(&mut self) -> Result { + if self.peek_token_ref().token == Token::LParen { + let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_prefix())?; + Ok(expr) + } else { + Ok(self.parse_expr()?) + } + } + pub(crate) fn parse_tag(&mut self) -> Result { - let name = self.parse_identifier()?; + let name = self.parse_object_name(false)?; self.expect_token(&Token::Eq)?; let value = self.parse_literal_string()?; @@ -7538,7 +8210,7 @@ impl<'a> Parser<'a> { })) } else if self.parse_keywords(&[Keyword::ALWAYS, Keyword::AS]) { if self.expect_token(&Token::LParen).is_ok() { - let expr = self.parse_expr()?; + let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_expr())?; self.expect_token(&Token::RParen)?; let (gen_as, expr_mode) = if self.parse_keywords(&[Keyword::STORED]) { Ok(( @@ -7703,10 +8375,10 @@ impl<'a> Parser<'a> { let nulls_distinct = self.parse_optional_nulls_distinct()?; // optional index name - let index_name = self.parse_optional_indent()?; + let index_name = self.parse_optional_ident()?; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::Unique { @@ -7725,10 +8397,10 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::KEY)?; // optional index name - let index_name = self.parse_optional_indent()?; + let index_name = self.parse_optional_ident()?; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::PrimaryKey { @@ -7742,6 +8414,7 @@ impl<'a> Parser<'a> { } Token::Word(w) if w.keyword == Keyword::FOREIGN => { self.expect_keyword_is(Keyword::KEY)?; + let index_name = self.parse_optional_ident()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; self.expect_keyword_is(Keyword::REFERENCES)?; let foreign_table = self.parse_object_name(false)?; @@ -7764,6 +8437,7 @@ impl<'a> Parser<'a> { Ok(Some(TableConstraint::ForeignKey { name, + index_name, columns, foreign_table, referred_columns, @@ -7776,7 +8450,20 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let expr = Box::new(self.parse_expr()?); self.expect_token(&Token::RParen)?; - Ok(Some(TableConstraint::Check { name, expr })) + + let enforced = if self.parse_keyword(Keyword::ENFORCED) { + Some(true) + } else if self.parse_keywords(&[Keyword::NOT, Keyword::ENFORCED]) { + Some(false) + } else { + None + }; + + Ok(Some(TableConstraint::Check { + name, + expr, + enforced, + })) } Token::Word(w) if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) @@ -7787,17 +8474,19 @@ impl<'a> Parser<'a> { let name = match self.peek_token().token { Token::Word(word) if word.keyword == Keyword::USING => None, - _ => self.parse_optional_indent()?, + _ => self.parse_optional_ident()?, }; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; + let index_options = self.parse_index_options()?; Ok(Some(TableConstraint::Index { display_as_key, name, index_type, columns, + index_options, })) } Token::Word(w) @@ -7818,9 +8507,9 @@ impl<'a> Parser<'a> { let index_type_display = self.parse_index_type_display(); - let opt_index_name = self.parse_optional_indent()?; + let opt_index_name = self.parse_optional_ident()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; Ok(Some(TableConstraint::FulltextOrSpatial { fulltext, @@ -7928,7 +8617,7 @@ impl<'a> Parser<'a> { /// Parse `[ident]`, mostly `ident` is name, like: /// `window_name`, `index_name`, ... - pub fn parse_optional_indent(&mut self) -> Result, ParserError> { + pub fn parse_optional_ident(&mut self) -> Result, ParserError> { self.maybe_parse(|parser| parser.parse_identifier()) } @@ -8087,7 +8776,11 @@ impl<'a> Parser<'a> { pub fn parse_alter_table_operation(&mut self) -> Result { let operation = if self.parse_keyword(Keyword::ADD) { if let Some(constraint) = self.parse_optional_table_constraint()? { - AlterTableOperation::AddConstraint(constraint) + let not_valid = self.parse_keywords(&[Keyword::NOT, Keyword::VALID]); + AlterTableOperation::AddConstraint { + constraint, + not_valid, + } } else if dialect_of!(self is ClickHouseDialect|GenericDialect) && self.parse_keyword(Keyword::PROJECTION) { @@ -8139,7 +8832,14 @@ impl<'a> Parser<'a> { AlterTableOperation::RenameConstraint { old_name, new_name } } else if self.parse_keyword(Keyword::TO) { let table_name = self.parse_object_name(false)?; - AlterTableOperation::RenameTable { table_name } + AlterTableOperation::RenameTable { + table_name: RenameTableNameKind::To(table_name), + } + } else if self.parse_keyword(Keyword::AS) { + let table_name = self.parse_object_name(false)?; + AlterTableOperation::RenameTable { + table_name: RenameTableNameKind::As(table_name), + } } else { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let old_column_name = self.parse_identifier()?; @@ -8249,10 +8949,18 @@ impl<'a> Parser<'a> { drop_behavior, } } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) { - AlterTableOperation::DropPrimaryKey + let drop_behavior = self.parse_optional_drop_behavior(); + AlterTableOperation::DropPrimaryKey { drop_behavior } } else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::KEY]) { let name = self.parse_identifier()?; - AlterTableOperation::DropForeignKey { name } + let drop_behavior = self.parse_optional_drop_behavior(); + AlterTableOperation::DropForeignKey { + name, + drop_behavior, + } + } else if self.parse_keyword(Keyword::INDEX) { + let name = self.parse_identifier()?; + AlterTableOperation::DropIndex { name } } else if self.parse_keyword(Keyword::PROJECTION) && dialect_of!(self is ClickHouseDialect|GenericDialect) { @@ -8262,12 +8970,17 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::CLUSTERING, Keyword::KEY]) { AlterTableOperation::DropClusteringKey } else { - let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] + let has_column_keyword = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let column_name = self.parse_identifier()?; + let column_names = if self.dialect.supports_comma_separated_drop_column_list() { + self.parse_comma_separated(Parser::parse_identifier)? + } else { + vec![self.parse_identifier()?] + }; let drop_behavior = self.parse_optional_drop_behavior(); AlterTableOperation::DropColumn { - column_name, + has_column_keyword, + column_names, if_exists, drop_behavior, } @@ -8340,16 +9053,10 @@ impl<'a> Parser<'a> { } } else if self.parse_keywords(&[Keyword::DROP, Keyword::DEFAULT]) { AlterColumnOperation::DropDefault {} - } else if self.parse_keywords(&[Keyword::SET, Keyword::DATA, Keyword::TYPE]) - || (is_postgresql && self.parse_keyword(Keyword::TYPE)) - { - let data_type = self.parse_data_type()?; - let using = if is_postgresql && self.parse_keyword(Keyword::USING) { - Some(self.parse_expr()?) - } else { - None - }; - AlterColumnOperation::SetDataType { data_type, using } + } else if self.parse_keywords(&[Keyword::SET, Keyword::DATA, Keyword::TYPE]) { + self.parse_set_data_type(true)? + } else if self.parse_keyword(Keyword::TYPE) { + self.parse_set_data_type(false)? } else if self.parse_keywords(&[Keyword::ADD, Keyword::GENERATED]) { let generated_as = if self.parse_keyword(Keyword::ALWAYS) { Some(GeneratedAs::Always) @@ -8481,23 +9188,64 @@ impl<'a> Parser<'a> { let equals = self.consume_token(&Token::Eq); let value = self.parse_number_value()?; AlterTableOperation::AutoIncrement { equals, value } + } else if self.parse_keywords(&[Keyword::REPLICA, Keyword::IDENTITY]) { + let identity = if self.parse_keyword(Keyword::NONE) { + ReplicaIdentity::None + } else if self.parse_keyword(Keyword::FULL) { + ReplicaIdentity::Full + } else if self.parse_keyword(Keyword::DEFAULT) { + ReplicaIdentity::Default + } else if self.parse_keywords(&[Keyword::USING, Keyword::INDEX]) { + ReplicaIdentity::Index(self.parse_identifier()?) + } else { + return self.expected( + "NONE, FULL, DEFAULT, or USING INDEX index_name after REPLICA IDENTITY", + self.peek_token(), + ); + }; + + AlterTableOperation::ReplicaIdentity { identity } + } else if self.parse_keywords(&[Keyword::VALIDATE, Keyword::CONSTRAINT]) { + let name = self.parse_identifier()?; + AlterTableOperation::ValidateConstraint { name } } else { - let options: Vec = + let mut options = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; if !options.is_empty() { AlterTableOperation::SetTblProperties { table_properties: options, } } else { - return self.expected( - "ADD, RENAME, PARTITION, SWAP, DROP, or SET TBLPROPERTIES after ALTER TABLE", + options = self.parse_options(Keyword::SET)?; + if !options.is_empty() { + AlterTableOperation::SetOptionsParens { options } + } else { + return self.expected( + "ADD, RENAME, PARTITION, SWAP, DROP, REPLICA IDENTITY, SET, or SET TBLPROPERTIES after ALTER TABLE", self.peek_token(), - ); + ); + } } }; Ok(operation) } + fn parse_set_data_type(&mut self, had_set: bool) -> Result { + let data_type = self.parse_data_type()?; + let using = if self.dialect.supports_alter_column_type_using() + && self.parse_keyword(Keyword::USING) + { + Some(self.parse_expr()?) + } else { + None + }; + Ok(AlterColumnOperation::SetDataType { + data_type, + using, + had_set, + }) + } + fn parse_part_or_partition(&mut self) -> Result { let keyword = self.expect_one_of_keywords(&[Keyword::PART, Keyword::PARTITION])?; match keyword { @@ -8517,38 +9265,21 @@ impl<'a> Parser<'a> { Keyword::ROLE, Keyword::POLICY, Keyword::CONNECTOR, + Keyword::ICEBERG, + Keyword::SCHEMA, ])?; match object_type { + Keyword::SCHEMA => { + self.prev_token(); + self.prev_token(); + self.parse_alter_schema() + } Keyword::VIEW => self.parse_alter_view(), Keyword::TYPE => self.parse_alter_type(), - Keyword::TABLE => { - let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let only = self.parse_keyword(Keyword::ONLY); // [ ONLY ] - let table_name = self.parse_object_name(false)?; - let on_cluster = self.parse_optional_on_cluster()?; - let operations = self.parse_comma_separated(Parser::parse_alter_table_operation)?; - - let mut location = None; - if self.parse_keyword(Keyword::LOCATION) { - location = Some(HiveSetLocation { - has_set: false, - location: self.parse_identifier()?, - }); - } else if self.parse_keywords(&[Keyword::SET, Keyword::LOCATION]) { - location = Some(HiveSetLocation { - has_set: true, - location: self.parse_identifier()?, - }); - } - - Ok(Statement::AlterTable { - name: table_name, - if_exists, - only, - operations, - location, - on_cluster, - }) + Keyword::TABLE => self.parse_alter_table(false), + Keyword::ICEBERG => { + self.expect_keyword(Keyword::TABLE)?; + self.parse_alter_table(true) } Keyword::INDEX => { let index_name = self.parse_object_name(false)?; @@ -8576,6 +9307,45 @@ impl<'a> Parser<'a> { } } + /// Parse a [Statement::AlterTable] + pub fn parse_alter_table(&mut self, iceberg: bool) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let only = self.parse_keyword(Keyword::ONLY); // [ ONLY ] + let table_name = self.parse_object_name(false)?; + let on_cluster = self.parse_optional_on_cluster()?; + let operations = self.parse_comma_separated(Parser::parse_alter_table_operation)?; + + let mut location = None; + if self.parse_keyword(Keyword::LOCATION) { + location = Some(HiveSetLocation { + has_set: false, + location: self.parse_identifier()?, + }); + } else if self.parse_keywords(&[Keyword::SET, Keyword::LOCATION]) { + location = Some(HiveSetLocation { + has_set: true, + location: self.parse_identifier()?, + }); + } + + let end_token = if self.peek_token_ref().token == Token::SemiColon { + self.peek_token_ref().clone() + } else { + self.get_current_token().clone() + }; + + Ok(Statement::AlterTable { + name: table_name, + if_exists, + only, + operations, + location, + on_cluster, + iceberg, + end_token: AttachedToken(end_token), + }) + } + pub fn parse_alter_view(&mut self) -> Result { let name = self.parse_object_name(false)?; let columns = self.parse_parenthesized_column_list(Optional, false)?; @@ -8635,13 +9405,47 @@ impl<'a> Parser<'a> { }), })) } else { - return self.expected_ref( + self.expected_ref( "{RENAME TO | { RENAME | ADD } VALUE}", self.peek_token_ref(), - ); + ) } } + // Parse a [Statement::AlterSchema] + // ALTER SCHEMA [ IF EXISTS ] schema_name + pub fn parse_alter_schema(&mut self) -> Result { + self.expect_keywords(&[Keyword::ALTER, Keyword::SCHEMA])?; + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + let operation = if self.parse_keywords(&[Keyword::SET, Keyword::OPTIONS]) { + self.prev_token(); + let options = self.parse_options(Keyword::OPTIONS)?; + AlterSchemaOperation::SetOptionsParens { options } + } else if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT, Keyword::COLLATE]) { + let collate = self.parse_expr()?; + AlterSchemaOperation::SetDefaultCollate { collate } + } else if self.parse_keywords(&[Keyword::ADD, Keyword::REPLICA]) { + let replica = self.parse_identifier()?; + let options = if self.peek_keyword(Keyword::OPTIONS) { + Some(self.parse_options(Keyword::OPTIONS)?) + } else { + None + }; + AlterSchemaOperation::AddReplica { replica, options } + } else if self.parse_keywords(&[Keyword::DROP, Keyword::REPLICA]) { + let replica = self.parse_identifier()?; + AlterSchemaOperation::DropReplica { replica } + } else { + return self.expected_ref("ALTER SCHEMA operation", self.peek_token_ref()); + }; + Ok(Statement::AlterSchema(AlterSchema { + name, + if_exists, + operations: vec![operation], + })) + } + /// Parse a `CALL procedure_name(arg1, arg2, ...)` /// or `CALL procedure_name` statement pub fn parse_call(&mut self) -> Result { @@ -8735,6 +9539,14 @@ impl<'a> Parser<'a> { }) } + /// Parse [Statement::Open] + fn parse_open(&mut self) -> Result { + self.expect_keyword(Keyword::OPEN)?; + Ok(Statement::Open(OpenStatement { + cursor_name: self.parse_identifier()?, + })) + } + pub fn parse_close(&mut self) -> Result { let cursor = if self.parse_keyword(Keyword::ALL) { CloseCursor::All @@ -8790,21 +9602,38 @@ impl<'a> Parser<'a> { } fn parse_copy_legacy_option(&mut self) -> Result { + // FORMAT \[ AS \] is optional + if self.parse_keyword(Keyword::FORMAT) { + let _ = self.parse_keyword(Keyword::AS); + } + let ret = match self.parse_one_of_keywords(&[ + Keyword::ACCEPTANYDATE, + Keyword::ACCEPTINVCHARS, Keyword::BINARY, + Keyword::BLANKSASNULL, + Keyword::CSV, + Keyword::DATEFORMAT, Keyword::DELIMITER, + Keyword::EMPTYASNULL, + Keyword::IAM_ROLE, + Keyword::IGNOREHEADER, Keyword::NULL, - Keyword::CSV, + Keyword::TIMEFORMAT, + Keyword::TRUNCATECOLUMNS, ]) { - Some(Keyword::BINARY) => CopyLegacyOption::Binary, - Some(Keyword::DELIMITER) => { - let _ = self.parse_keyword(Keyword::AS); // [ AS ] - CopyLegacyOption::Delimiter(self.parse_literal_char()?) - } - Some(Keyword::NULL) => { + Some(Keyword::ACCEPTANYDATE) => CopyLegacyOption::AcceptAnyDate, + Some(Keyword::ACCEPTINVCHARS) => { let _ = self.parse_keyword(Keyword::AS); // [ AS ] - CopyLegacyOption::Null(self.parse_literal_string()?) + let ch = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) { + Some(self.parse_literal_string()?) + } else { + None + }; + CopyLegacyOption::AcceptInvChars(ch) } + Some(Keyword::BINARY) => CopyLegacyOption::Binary, + Some(Keyword::BLANKSASNULL) => CopyLegacyOption::BlankAsNull, Some(Keyword::CSV) => CopyLegacyOption::Csv({ let mut opts = vec![]; while let Some(opt) = @@ -8814,11 +9643,54 @@ impl<'a> Parser<'a> { } opts }), + Some(Keyword::DATEFORMAT) => { + let _ = self.parse_keyword(Keyword::AS); + let fmt = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) { + Some(self.parse_literal_string()?) + } else { + None + }; + CopyLegacyOption::DateFormat(fmt) + } + Some(Keyword::DELIMITER) => { + let _ = self.parse_keyword(Keyword::AS); + CopyLegacyOption::Delimiter(self.parse_literal_char()?) + } + Some(Keyword::EMPTYASNULL) => CopyLegacyOption::EmptyAsNull, + Some(Keyword::IAM_ROLE) => CopyLegacyOption::IamRole(self.parse_iam_role_kind()?), + Some(Keyword::IGNOREHEADER) => { + let _ = self.parse_keyword(Keyword::AS); + let num_rows = self.parse_literal_uint()?; + CopyLegacyOption::IgnoreHeader(num_rows) + } + Some(Keyword::NULL) => { + let _ = self.parse_keyword(Keyword::AS); + CopyLegacyOption::Null(self.parse_literal_string()?) + } + Some(Keyword::TIMEFORMAT) => { + let _ = self.parse_keyword(Keyword::AS); + let fmt = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) { + Some(self.parse_literal_string()?) + } else { + None + }; + CopyLegacyOption::TimeFormat(fmt) + } + Some(Keyword::TRUNCATECOLUMNS) => CopyLegacyOption::TruncateColumns, _ => self.expected("option", self.peek_token())?, }; Ok(ret) } + fn parse_iam_role_kind(&mut self) -> Result { + if self.parse_keyword(Keyword::DEFAULT) { + Ok(IamRoleKind::Default) + } else { + let arn = self.parse_literal_string()?; + Ok(IamRoleKind::Arn(arn)) + } + } + fn parse_copy_legacy_csv_option(&mut self) -> Result { let ret = match self.parse_one_of_keywords(&[ Keyword::HEADER, @@ -8981,16 +9853,21 @@ impl<'a> Parser<'a> { Token::HexStringLiteral(ref s) => ok_value(Value::HexStringLiteral(s.to_string())), Token::Placeholder(ref s) => ok_value(Value::Placeholder(s.to_string())), tok @ Token::Colon | tok @ Token::AtSign => { - // Not calling self.parse_identifier(false)? because only in placeholder we want to check numbers as idfentifies - // This because snowflake allows numbers as placeholders - let next_token = self.next_token(); + // 1. Not calling self.parse_identifier(false)? + // because only in placeholder we want to check + // numbers as idfentifies. This because snowflake + // allows numbers as placeholders + // 2. Not calling self.next_token() to enforce `tok` + // be followed immediately by a word/number, ie. + // without any whitespace in between + let next_token = self.next_token_no_skip().unwrap_or(&EOF_TOKEN).clone(); let ident = match next_token.token { Token::Word(w) => Ok(w.into_ident(next_token.span)), - Token::Number(w, false) => Ok(Ident::new(w)), + Token::Number(w, false) => Ok(Ident::with_span(next_token.span, w)), _ => self.expected("placeholder", next_token), }?; - let placeholder = tok.to_string() + &ident.value; - ok_value(Value::Placeholder(placeholder)) + Ok(Value::Placeholder(tok.to_string() + &ident.value) + .with_span(Span::new(span.start, ident.span.end))) } unexpected => self.expected( "a value", @@ -9103,6 +9980,15 @@ impl<'a> Parser<'a> { } } + /// Parse a boolean string + pub(crate) fn parse_boolean_string(&mut self) -> Result { + match self.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) { + Some(Keyword::TRUE) => Ok(true), + Some(Keyword::FALSE) => Ok(false), + _ => self.expected("TRUE or FALSE", self.peek_token()), + } + } + /// Parse a literal unicode normalization clause pub fn parse_unicode_is_normalized(&mut self, expr: Expr) -> Result { let neg = self.parse_keyword(Keyword::NOT); @@ -9193,6 +10079,9 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::UNSIGNED) { Ok(DataType::TinyIntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::TinyInt(optional_precision?)) } } @@ -9209,6 +10098,9 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::UNSIGNED) { Ok(DataType::SmallIntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::SmallInt(optional_precision?)) } } @@ -9217,6 +10109,9 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::UNSIGNED) { Ok(DataType::MediumIntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::MediumInt(optional_precision?)) } } @@ -9225,6 +10120,9 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::UNSIGNED) { Ok(DataType::IntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::Int(optional_precision?)) } } @@ -9254,6 +10152,9 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::UNSIGNED) { Ok(DataType::IntegerUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::Integer(optional_precision?)) } } @@ -9262,6 +10163,9 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::UNSIGNED) { Ok(DataType::BigIntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::BigInt(optional_precision?)) } } @@ -9364,10 +10268,18 @@ impl<'a> Parser<'a> { self.parse_optional_precision()?, TimezoneInfo::Tz, )), - // Interval types can be followed by a complicated interval - // qualifier that we don't currently support. See - // parse_interval for a taste. - Keyword::INTERVAL => Ok(DataType::Interval), + Keyword::INTERVAL => { + if self.dialect.supports_interval_options() { + let fields = self.maybe_parse_optional_interval_fields()?; + let precision = self.parse_optional_precision()?; + Ok(DataType::Interval { fields, precision }) + } else { + Ok(DataType::Interval { + fields: None, + precision: None, + }) + } + } Keyword::JSON => Ok(DataType::JSON), Keyword::JSONB => Ok(DataType::JSONB), Keyword::REGCLASS => Ok(DataType::Regclass), @@ -9469,8 +10381,14 @@ impl<'a> Parser<'a> { Ok(DataType::AnyType) } Keyword::TABLE => { - let columns = self.parse_returns_table_columns()?; - Ok(DataType::Table(columns)) + // an LParen after the TABLE keyword indicates that table columns are being defined + // whereas no LParen indicates an anonymous table expression will be returned + if self.peek_token() == Token::LParen { + let columns = self.parse_returns_table_columns()?; + Ok(DataType::Table(Some(columns))) + } else { + Ok(DataType::Table(None)) + } } Keyword::SIGNED => { if self.parse_keyword(Keyword::INTEGER) { @@ -9486,6 +10404,12 @@ impl<'a> Parser<'a> { Ok(DataType::Unsigned) } } + Keyword::TSVECTOR if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { + Ok(DataType::TsVector) + } + Keyword::TSQUERY if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { + Ok(DataType::TsQuery) + } _ => { self.prev_token(); let type_name = self.parse_object_name(false)?; @@ -9511,13 +10435,7 @@ impl<'a> Parser<'a> { } fn parse_returns_table_column(&mut self) -> Result { - let name = self.parse_identifier()?; - let data_type = self.parse_data_type()?; - Ok(ColumnDef { - name, - data_type, - options: Vec::new(), // No constraints expected here - }) + self.parse_column_def() } fn parse_returns_table_columns(&mut self) -> Result, ParserError> { @@ -9554,6 +10472,48 @@ impl<'a> Parser<'a> { Ok(IdentWithAlias { ident, alias }) } + /// Parse `identifier [AS] identifier` where the AS keyword is optional + fn parse_identifier_with_optional_alias(&mut self) -> Result { + let ident = self.parse_identifier()?; + let _after_as = self.parse_keyword(Keyword::AS); + let alias = self.parse_identifier()?; + Ok(IdentWithAlias { ident, alias }) + } + + /// Parse comma-separated list of parenthesized queries for pipe operators + fn parse_pipe_operator_queries(&mut self) -> Result, ParserError> { + self.parse_comma_separated(|parser| { + parser.expect_token(&Token::LParen)?; + let query = parser.parse_query()?; + parser.expect_token(&Token::RParen)?; + Ok(*query) + }) + } + + /// Parse set quantifier for pipe operators that require DISTINCT. E.g. INTERSECT and EXCEPT + fn parse_distinct_required_set_quantifier( + &mut self, + operator_name: &str, + ) -> Result { + let quantifier = self.parse_set_quantifier(&Some(SetOperator::Intersect)); + match quantifier { + SetQuantifier::Distinct | SetQuantifier::DistinctByName => Ok(quantifier), + _ => Err(ParserError::ParserError(format!( + "{operator_name} pipe operator requires DISTINCT modifier", + ))), + } + } + + /// Parse optional identifier alias (with or without AS keyword) + fn parse_identifier_optional_alias(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::AS) { + Ok(Some(self.parse_identifier()?)) + } else { + // Check if the next token is an identifier (implicit alias) + self.maybe_parse(|parser| parser.parse_identifier()) + } + } + /// Optionally parses an alias for a select list item fn maybe_parse_select_item_alias(&mut self) -> Result, ParserError> { fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { @@ -9727,7 +10687,13 @@ impl<'a> Parser<'a> { } if self.parse_keywords(&[Keyword::GROUPING, Keyword::SETS]) { self.expect_token(&Token::LParen)?; - let result = self.parse_comma_separated(|p| p.parse_tuple(true, true))?; + let result = self.parse_comma_separated(|p| { + if p.peek_token_ref().token == Token::LParen { + p.parse_tuple(true, true) + } else { + Ok(vec![p.parse_expr()?]) + } + })?; self.expect_token(&Token::RParen)?; modifiers.push(GroupByWithModifier::GroupingSets(Expr::GroupingSets( result, @@ -9836,70 +10802,92 @@ impl<'a> Parser<'a> { } } - /// Parse a possibly qualified, possibly quoted identifier, optionally allowing for wildcards, + /// Parse a possibly qualified, possibly quoted identifier, e.g. + /// `foo` or `myschema."table" + /// + /// The `in_table_clause` parameter indicates whether the object name is a table in a FROM, JOIN, + /// or similar table clause. Currently, this is used only to support unquoted hyphenated identifiers + /// in this context on BigQuery. + pub fn parse_object_name(&mut self, in_table_clause: bool) -> Result { + self.parse_object_name_inner(in_table_clause, false) + } + + /// Parse a possibly qualified, possibly quoted identifier, e.g. + /// `foo` or `myschema."table" + /// + /// The `in_table_clause` parameter indicates whether the object name is a table in a FROM, JOIN, + /// or similar table clause. Currently, this is used only to support unquoted hyphenated identifiers + /// in this context on BigQuery. + /// + /// The `allow_wildcards` parameter indicates whether to allow for wildcards in the object name /// e.g. *, *.*, `foo`.*, or "foo"."bar" - fn parse_object_name_with_wildcards( + fn parse_object_name_inner( &mut self, in_table_clause: bool, allow_wildcards: bool, ) -> Result { - let mut idents = vec![]; - + let mut parts = vec![]; if dialect_of!(self is BigQueryDialect) && in_table_clause { loop { let (ident, end_with_period) = self.parse_unquoted_hyphenated_identifier()?; - idents.push(ident); + parts.push(ObjectNamePart::Identifier(ident)); if !self.consume_token(&Token::Period) && !end_with_period { break; } } } else { loop { - let ident = if allow_wildcards && self.peek_token().token == Token::Mul { + if allow_wildcards && self.peek_token().token == Token::Mul { let span = self.next_token().span; - Ident { + parts.push(ObjectNamePart::Identifier(Ident { value: Token::Mul.to_string(), quote_style: None, span, + })); + } else if dialect_of!(self is BigQueryDialect) && in_table_clause { + let (ident, end_with_period) = self.parse_unquoted_hyphenated_identifier()?; + parts.push(ObjectNamePart::Identifier(ident)); + if !self.consume_token(&Token::Period) && !end_with_period { + break; } + } else if self.dialect.supports_object_name_double_dot_notation() + && parts.len() == 1 + && matches!(self.peek_token().token, Token::Period) + { + // Empty string here means default schema + parts.push(ObjectNamePart::Identifier(Ident::new(""))); } else { - if self.dialect.supports_object_name_double_dot_notation() - && idents.len() == 1 - && self.consume_token(&Token::Period) + let ident = self.parse_identifier()?; + let part = if self + .dialect + .is_identifier_generating_function_name(&ident, &parts) { - // Empty string here means default schema - idents.push(Ident::new("")); - } - self.parse_identifier()? - }; - idents.push(ident); + self.expect_token(&Token::LParen)?; + let args: Vec = + self.parse_comma_separated0(Self::parse_function_args, Token::RParen)?; + self.expect_token(&Token::RParen)?; + ObjectNamePart::Function(ObjectNamePartFunction { name: ident, args }) + } else { + ObjectNamePart::Identifier(ident) + }; + parts.push(part); + } + if !self.consume_token(&Token::Period) { break; } } } - Ok(ObjectName::from(idents)) - } - - /// Parse a possibly qualified, possibly quoted identifier, e.g. - /// `foo` or `myschema."table" - /// - /// The `in_table_clause` parameter indicates whether the object name is a table in a FROM, JOIN, - /// or similar table clause. Currently, this is used only to support unquoted hyphenated identifiers - /// in this context on BigQuery. - pub fn parse_object_name(&mut self, in_table_clause: bool) -> Result { - let ObjectName(mut idents) = - self.parse_object_name_with_wildcards(in_table_clause, false)?; // BigQuery accepts any number of quoted identifiers of a table name. // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#quoted_identifiers if dialect_of!(self is BigQueryDialect) - && idents.iter().any(|part| { + && parts.iter().any(|part| { part.as_ident() .is_some_and(|ident| ident.value.contains('.')) }) { - idents = idents + parts = parts .into_iter() .flat_map(|part| match part.as_ident() { Some(ident) => ident @@ -9918,7 +10906,7 @@ impl<'a> Parser<'a> { .collect() } - Ok(ObjectName(idents)) + Ok(ObjectName(parts)) } /// Parse identifiers @@ -10139,17 +11127,7 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier()?; - let options = if (dialect_of!(self is BigQueryDialect | GenericDialect) - && self.parse_keyword(Keyword::OPTIONS)) - || (dialect_of!(self is SnowflakeDialect | GenericDialect) - && self.parse_keyword(Keyword::COMMENT)) - { - self.prev_token(); - self.parse_optional_column_option()? - .map(|option| vec![option]) - } else { - None - }; + let options = self.parse_view_column_options()?; let data_type = if dialect_of!(self is ClickHouseDialect) { Some(self.parse_data_type()?) } else { @@ -10162,6 +11140,25 @@ impl<'a> Parser<'a> { }) } + fn parse_view_column_options(&mut self) -> Result, ParserError> { + let mut options = Vec::new(); + loop { + let option = self.parse_optional_column_option()?; + if let Some(option) = option { + options.push(option); + } else { + break; + } + } + if options.is_empty() { + Ok(None) + } else if self.dialect.supports_space_separated_column_options() { + Ok(Some(ColumnOptions::SpaceSeparated(options))) + } else { + Ok(Some(ColumnOptions::CommaSeparated(options))) + } + } + /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. /// For example: `(col1, "col 2", ...)` pub fn parse_parenthesized_column_list( @@ -10172,6 +11169,26 @@ impl<'a> Parser<'a> { self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| p.parse_identifier()) } + pub fn parse_parenthesized_compound_identifier_list( + &mut self, + optional: IsOptional, + allow_empty: bool, + ) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { + Ok(Expr::CompoundIdentifier( + p.parse_period_separated(|p| p.parse_identifier())?, + )) + }) + } + + /// Parses a parenthesized comma-separated list of index columns, which can be arbitrary + /// expressions with ordering information (and an opclass in some dialects). + fn parse_parenthesized_index_column_list(&mut self) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(Mandatory, false, |p| { + p.parse_create_index_expr() + }) + } + /// Parses a parenthesized comma-separated list of qualified, possibly quoted identifiers. /// For example: `(db1.sc1.tbl1.col1, db1.sc1.tbl1."col 2", ...)` pub fn parse_parenthesized_qualified_column_list( @@ -10243,6 +11260,85 @@ impl<'a> Parser<'a> { } } + fn maybe_parse_optional_interval_fields( + &mut self, + ) -> Result, ParserError> { + match self.parse_one_of_keywords(&[ + // Can be followed by `TO` option + Keyword::YEAR, + Keyword::DAY, + Keyword::HOUR, + Keyword::MINUTE, + // No `TO` option + Keyword::MONTH, + Keyword::SECOND, + ]) { + Some(Keyword::YEAR) => { + if self.peek_keyword(Keyword::TO) { + self.expect_keyword(Keyword::TO)?; + self.expect_keyword(Keyword::MONTH)?; + Ok(Some(IntervalFields::YearToMonth)) + } else { + Ok(Some(IntervalFields::Year)) + } + } + Some(Keyword::DAY) => { + if self.peek_keyword(Keyword::TO) { + self.expect_keyword(Keyword::TO)?; + match self.expect_one_of_keywords(&[ + Keyword::HOUR, + Keyword::MINUTE, + Keyword::SECOND, + ])? { + Keyword::HOUR => Ok(Some(IntervalFields::DayToHour)), + Keyword::MINUTE => Ok(Some(IntervalFields::DayToMinute)), + Keyword::SECOND => Ok(Some(IntervalFields::DayToSecond)), + _ => { + self.prev_token(); + self.expected("HOUR, MINUTE, or SECOND", self.peek_token()) + } + } + } else { + Ok(Some(IntervalFields::Day)) + } + } + Some(Keyword::HOUR) => { + if self.peek_keyword(Keyword::TO) { + self.expect_keyword(Keyword::TO)?; + match self.expect_one_of_keywords(&[Keyword::MINUTE, Keyword::SECOND])? { + Keyword::MINUTE => Ok(Some(IntervalFields::HourToMinute)), + Keyword::SECOND => Ok(Some(IntervalFields::HourToSecond)), + _ => { + self.prev_token(); + self.expected("MINUTE or SECOND", self.peek_token()) + } + } + } else { + Ok(Some(IntervalFields::Hour)) + } + } + Some(Keyword::MINUTE) => { + if self.peek_keyword(Keyword::TO) { + self.expect_keyword(Keyword::TO)?; + self.expect_keyword(Keyword::SECOND)?; + Ok(Some(IntervalFields::MinuteToSecond)) + } else { + Ok(Some(IntervalFields::Minute)) + } + } + Some(Keyword::MONTH) => Ok(Some(IntervalFields::Month)), + Some(Keyword::SECOND) => Ok(Some(IntervalFields::Second)), + Some(_) => { + self.prev_token(); + self.expected( + "YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND", + self.peek_token(), + ) + } + None => Ok(None), + } + } + /// Parse datetime64 [1] /// Syntax /// ```sql @@ -10331,7 +11427,7 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::LParen) { let precision = self.parse_literal_uint()?; let scale = if self.consume_token(&Token::Comma) { - Some(self.parse_literal_uint()?) + Some(self.parse_signed_integer()?) } else { None }; @@ -10347,6 +11443,27 @@ impl<'a> Parser<'a> { } } + /// Parse an optionally signed integer literal. + fn parse_signed_integer(&mut self) -> Result { + let is_negative = self.consume_token(&Token::Minus); + + if !is_negative { + let _ = self.consume_token(&Token::Plus); + } + + let current_token = self.peek_token_ref(); + match ¤t_token.token { + Token::Number(s, _) => { + let s = s.clone(); + let span_start = current_token.span.start; + self.advance_token(); + let value = Self::parse::(s, span_start)?; + Ok(if is_negative { -value } else { value }) + } + _ => self.expected_ref("number", current_token), + } + } + pub fn parse_optional_type_modifiers(&mut self) -> Result>, ParserError> { if self.consume_token(&Token::LParen) { let mut modifiers = Vec::new(); @@ -10391,6 +11508,13 @@ impl<'a> Parser<'a> { Ok(Box::new(SetExpr::Delete(self.parse_delete()?))) } + /// Parse a MERGE statement, returning a `Box`ed SetExpr + /// + /// This is used to reduce the size of the stack frames in debug builds + fn parse_merge_setexpr_boxed(&mut self) -> Result, ParserError> { + Ok(Box::new(SetExpr::Merge(self.parse_merge()?))) + } + pub fn parse_delete(&mut self) -> Result { let (tables, with_from_keyword) = if !self.parse_keyword(Keyword::FROM) { // `FROM` keyword is optional in BigQuery SQL. @@ -10500,7 +11624,7 @@ impl<'a> Parser<'a> { analyze = self.parse_keyword(Keyword::ANALYZE); verbose = self.parse_keyword(Keyword::VERBOSE); if self.parse_keyword(Keyword::FORMAT) { - format = Some(self.parse_analyze_format()?); + format = Some(self.parse_analyze_format_kind()?); } } @@ -10571,6 +11695,7 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], } .into()) } else if self.parse_keyword(Keyword::UPDATE) { @@ -10584,6 +11709,7 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], } .into()) } else if self.parse_keyword(Keyword::DELETE) { @@ -10597,6 +11723,21 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], + } + .into()) + } else if self.parse_keyword(Keyword::MERGE) { + Ok(Query { + with, + body: self.parse_merge_setexpr_boxed()?, + limit_clause: None, + order_by: None, + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + pipe_operators: vec![], } .into()) } else { @@ -10637,6 +11778,12 @@ impl<'a> Parser<'a> { None }; + let pipe_operators = if self.dialect.supports_pipe_operator() { + self.parse_pipe_operators()? + } else { + Vec::new() + }; + Ok(Query { with, body, @@ -10647,11 +11794,231 @@ impl<'a> Parser<'a> { for_clause, settings, format_clause, + pipe_operators, } .into()) } } + fn parse_pipe_operators(&mut self) -> Result, ParserError> { + let mut pipe_operators = Vec::new(); + + while self.consume_token(&Token::VerticalBarRightAngleBracket) { + let kw = self.expect_one_of_keywords(&[ + Keyword::SELECT, + Keyword::EXTEND, + Keyword::SET, + Keyword::DROP, + Keyword::AS, + Keyword::WHERE, + Keyword::LIMIT, + Keyword::AGGREGATE, + Keyword::ORDER, + Keyword::TABLESAMPLE, + Keyword::RENAME, + Keyword::UNION, + Keyword::INTERSECT, + Keyword::EXCEPT, + Keyword::CALL, + Keyword::PIVOT, + Keyword::UNPIVOT, + Keyword::JOIN, + Keyword::INNER, + Keyword::LEFT, + Keyword::RIGHT, + Keyword::FULL, + Keyword::CROSS, + ])?; + match kw { + Keyword::SELECT => { + let exprs = self.parse_comma_separated(Parser::parse_select_item)?; + pipe_operators.push(PipeOperator::Select { exprs }) + } + Keyword::EXTEND => { + let exprs = self.parse_comma_separated(Parser::parse_select_item)?; + pipe_operators.push(PipeOperator::Extend { exprs }) + } + Keyword::SET => { + let assignments = self.parse_comma_separated(Parser::parse_assignment)?; + pipe_operators.push(PipeOperator::Set { assignments }) + } + Keyword::DROP => { + let columns = self.parse_identifiers()?; + pipe_operators.push(PipeOperator::Drop { columns }) + } + Keyword::AS => { + let alias = self.parse_identifier()?; + pipe_operators.push(PipeOperator::As { alias }) + } + Keyword::WHERE => { + let expr = self.parse_expr()?; + pipe_operators.push(PipeOperator::Where { expr }) + } + Keyword::LIMIT => { + let expr = self.parse_expr()?; + let offset = if self.parse_keyword(Keyword::OFFSET) { + Some(self.parse_expr()?) + } else { + None + }; + pipe_operators.push(PipeOperator::Limit { expr, offset }) + } + Keyword::AGGREGATE => { + let full_table_exprs = if self.peek_keyword(Keyword::GROUP) { + vec![] + } else { + self.parse_comma_separated(|parser| { + parser.parse_expr_with_alias_and_order_by() + })? + }; + + let group_by_expr = if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { + self.parse_comma_separated(|parser| { + parser.parse_expr_with_alias_and_order_by() + })? + } else { + vec![] + }; + + pipe_operators.push(PipeOperator::Aggregate { + full_table_exprs, + group_by_expr, + }) + } + Keyword::ORDER => { + self.expect_one_of_keywords(&[Keyword::BY])?; + let exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; + pipe_operators.push(PipeOperator::OrderBy { exprs }) + } + Keyword::TABLESAMPLE => { + let sample = self.parse_table_sample(TableSampleModifier::TableSample)?; + pipe_operators.push(PipeOperator::TableSample { sample }); + } + Keyword::RENAME => { + let mappings = + self.parse_comma_separated(Parser::parse_identifier_with_optional_alias)?; + pipe_operators.push(PipeOperator::Rename { mappings }); + } + Keyword::UNION => { + let set_quantifier = self.parse_set_quantifier(&Some(SetOperator::Union)); + let queries = self.parse_pipe_operator_queries()?; + pipe_operators.push(PipeOperator::Union { + set_quantifier, + queries, + }); + } + Keyword::INTERSECT => { + let set_quantifier = + self.parse_distinct_required_set_quantifier("INTERSECT")?; + let queries = self.parse_pipe_operator_queries()?; + pipe_operators.push(PipeOperator::Intersect { + set_quantifier, + queries, + }); + } + Keyword::EXCEPT => { + let set_quantifier = self.parse_distinct_required_set_quantifier("EXCEPT")?; + let queries = self.parse_pipe_operator_queries()?; + pipe_operators.push(PipeOperator::Except { + set_quantifier, + queries, + }); + } + Keyword::CALL => { + let function_name = self.parse_object_name(false)?; + let function_expr = self.parse_function(function_name)?; + if let Expr::Function(function) = function_expr { + let alias = self.parse_identifier_optional_alias()?; + pipe_operators.push(PipeOperator::Call { function, alias }); + } else { + return Err(ParserError::ParserError( + "Expected function call after CALL".to_string(), + )); + } + } + Keyword::PIVOT => { + self.expect_token(&Token::LParen)?; + let aggregate_functions = + self.parse_comma_separated(Self::parse_aliased_function_call)?; + self.expect_keyword_is(Keyword::FOR)?; + let value_column = self.parse_period_separated(|p| p.parse_identifier())?; + self.expect_keyword_is(Keyword::IN)?; + + self.expect_token(&Token::LParen)?; + let value_source = if self.parse_keyword(Keyword::ANY) { + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_order_by_expr)? + } else { + vec![] + }; + PivotValueSource::Any(order_by) + } else if self.peek_sub_query() { + PivotValueSource::Subquery(self.parse_query()?) + } else { + PivotValueSource::List( + self.parse_comma_separated(Self::parse_expr_with_alias)?, + ) + }; + self.expect_token(&Token::RParen)?; + self.expect_token(&Token::RParen)?; + + let alias = self.parse_identifier_optional_alias()?; + + pipe_operators.push(PipeOperator::Pivot { + aggregate_functions, + value_column, + value_source, + alias, + }); + } + Keyword::UNPIVOT => { + self.expect_token(&Token::LParen)?; + let value_column = self.parse_identifier()?; + self.expect_keyword(Keyword::FOR)?; + let name_column = self.parse_identifier()?; + self.expect_keyword(Keyword::IN)?; + + self.expect_token(&Token::LParen)?; + let unpivot_columns = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_token(&Token::RParen)?; + + self.expect_token(&Token::RParen)?; + + let alias = self.parse_identifier_optional_alias()?; + + pipe_operators.push(PipeOperator::Unpivot { + value_column, + name_column, + unpivot_columns, + alias, + }); + } + Keyword::JOIN + | Keyword::INNER + | Keyword::LEFT + | Keyword::RIGHT + | Keyword::FULL + | Keyword::CROSS => { + self.prev_token(); + let mut joins = self.parse_joins()?; + if joins.len() != 1 { + return Err(ParserError::ParserError( + "Join pipe operator must have a single join".to_string(), + )); + } + let join = joins.swap_remove(0); + pipe_operators.push(PipeOperator::Join(join)) + } + unhandled => { + return Err(ParserError::ParserError(format!( + "`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}" + ))) + } + } + } + Ok(pipe_operators) + } + fn parse_settings(&mut self) -> Result>, ParserError> { let settings = if dialect_of!(self is ClickHouseDialect|GenericDialect) && self.parse_keyword(Keyword::SETTINGS) @@ -10659,7 +12026,7 @@ impl<'a> Parser<'a> { let key_values = self.parse_comma_separated(|p| { let key = p.parse_identifier()?; p.expect_token(&Token::Eq)?; - let value = p.parse_value()?.value; + let value = p.parse_expr()?; Ok(Setting { key, value }) })?; Some(key_values) @@ -10955,6 +12322,7 @@ impl<'a> Parser<'a> { top: None, top_before_distinct: false, projection: vec![], + exclude: None, into: None, from, lateral_views: vec![], @@ -10977,18 +12345,7 @@ impl<'a> Parser<'a> { } let select_token = self.expect_keyword(Keyword::SELECT)?; - let value_table_mode = - if dialect_of!(self is BigQueryDialect) && self.parse_keyword(Keyword::AS) { - if self.parse_keyword(Keyword::VALUE) { - Some(ValueTableMode::AsValue) - } else if self.parse_keyword(Keyword::STRUCT) { - Some(ValueTableMode::AsStruct) - } else { - self.expected("VALUE or STRUCT", self.peek_token())? - } - } else { - None - }; + let value_table_mode = self.parse_value_table_mode()?; let mut top_before_distinct = false; let mut top = None; @@ -11008,6 +12365,12 @@ impl<'a> Parser<'a> { self.parse_projection()? }; + let exclude = if self.dialect.supports_select_exclude() { + self.parse_optional_select_item_exclude()? + } else { + None + }; + let into = if self.parse_keyword(Keyword::INTO) { Some(self.parse_select_into()?) } else { @@ -11089,7 +12452,7 @@ impl<'a> Parser<'a> { }; let sort_by = if self.parse_keywords(&[Keyword::SORT, Keyword::BY]) { - self.parse_comma_separated(Parser::parse_expr)? + self.parse_comma_separated(Parser::parse_order_by_expr)? } else { vec![] }; @@ -11141,6 +12504,7 @@ impl<'a> Parser<'a> { top, top_before_distinct, projection, + exclude, into, from, lateral_views, @@ -11164,6 +12528,32 @@ impl<'a> Parser<'a> { }) } + fn parse_value_table_mode(&mut self) -> Result, ParserError> { + if !dialect_of!(self is BigQueryDialect) { + return Ok(None); + } + + let mode = if self.parse_keywords(&[Keyword::DISTINCT, Keyword::AS, Keyword::VALUE]) { + Some(ValueTableMode::DistinctAsValue) + } else if self.parse_keywords(&[Keyword::DISTINCT, Keyword::AS, Keyword::STRUCT]) { + Some(ValueTableMode::DistinctAsStruct) + } else if self.parse_keywords(&[Keyword::AS, Keyword::VALUE]) + || self.parse_keywords(&[Keyword::ALL, Keyword::AS, Keyword::VALUE]) + { + Some(ValueTableMode::AsValue) + } else if self.parse_keywords(&[Keyword::AS, Keyword::STRUCT]) + || self.parse_keywords(&[Keyword::ALL, Keyword::AS, Keyword::STRUCT]) + { + Some(ValueTableMode::AsStruct) + } else if self.parse_keyword(Keyword::AS) { + self.expected("VALUE or STRUCT", self.peek_token())? + } else { + None + }; + + Ok(mode) + } + /// Invoke `f` after first setting the parser's `ParserState` to `state`. /// /// Upon return, restores the parser's state to what it started at. @@ -11597,6 +12987,10 @@ impl<'a> Parser<'a> { self.parse_show_databases(terse) } else if self.parse_keyword(Keyword::SCHEMAS) { self.parse_show_schemas(terse) + } else if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) { + self.parse_show_charset(false) + } else if self.parse_keyword(Keyword::CHARSET) { + self.parse_show_charset(true) } else { Ok(Statement::ShowVariable { variable: self.parse_identifiers()?, @@ -11604,6 +12998,14 @@ impl<'a> Parser<'a> { } } + fn parse_show_charset(&mut self, is_shorthand: bool) -> Result { + // parse one of keywords + Ok(Statement::ShowCharset(ShowCharset { + is_shorthand, + filter: self.parse_show_statement_filter()?, + })) + } + fn parse_show_databases(&mut self, terse: bool) -> Result { let history = self.parse_keyword(Keyword::HISTORY); let show_options = self.parse_show_stmt_options()?; @@ -11922,7 +13324,11 @@ impl<'a> Parser<'a> { }; let mut relation = self.parse_table_factor()?; - if self.peek_parens_less_nested_join() { + if !self + .dialect + .supports_left_associative_joins_without_parens() + && self.peek_parens_less_nested_join() + { let joins = self.parse_joins()?; relation = TableFactor::NestedJoin { table_with_joins: Box::new(TableWithJoins { relation, joins }), @@ -12070,6 +13476,7 @@ impl<'a> Parser<'a> { | TableFactor::Pivot { alias, .. } | TableFactor::Unpivot { alias, .. } | TableFactor::MatchRecognize { alias, .. } + | TableFactor::SemanticView { alias, .. } | TableFactor::NestedJoin { alias, .. } => { // but not `FROM (mytable AS alias1) AS alias2`. if let Some(inner_alias) = alias { @@ -12122,6 +13529,7 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), alias, }) @@ -12183,6 +13591,10 @@ impl<'a> Parser<'a> { } else if self.parse_keyword_with_tokens(Keyword::XMLTABLE, &[Token::LParen]) { self.prev_token(); self.parse_xml_table_factor() + } else if self.dialect.supports_semantic_view_table_factor() + && self.peek_keyword_with_tokens(Keyword::SEMANTIC_VIEW, &[Token::LParen]) + { + self.parse_semantic_view_table_factor() } else { let name = self.parse_object_name(true)?; @@ -12285,7 +13697,13 @@ impl<'a> Parser<'a> { } else { return Ok(None); }; + self.parse_table_sample(modifier).map(Some) + } + fn parse_table_sample( + &mut self, + modifier: TableSampleModifier, + ) -> Result, ParserError> { let name = match self.parse_one_of_keywords(&[ Keyword::BERNOULLI, Keyword::ROW, @@ -12367,14 +13785,14 @@ impl<'a> Parser<'a> { None }; - Ok(Some(Box::new(TableSample { + Ok(Box::new(TableSample { modifier, name, quantity, seed, bucket, offset, - }))) + })) } fn parse_table_sample_seed( @@ -12508,6 +13926,70 @@ impl<'a> Parser<'a> { Ok(XmlPassingClause { arguments }) } + /// Parse a [TableFactor::SemanticView] + fn parse_semantic_view_table_factor(&mut self) -> Result { + self.expect_keyword(Keyword::SEMANTIC_VIEW)?; + self.expect_token(&Token::LParen)?; + + let name = self.parse_object_name(true)?; + + // Parse DIMENSIONS, METRICS, FACTS and WHERE clauses in flexible order + let mut dimensions = Vec::new(); + let mut metrics = Vec::new(); + let mut facts = Vec::new(); + let mut where_clause = None; + + while self.peek_token().token != Token::RParen { + if self.parse_keyword(Keyword::DIMENSIONS) { + if !dimensions.is_empty() { + return Err(ParserError::ParserError( + "DIMENSIONS clause can only be specified once".to_string(), + )); + } + dimensions = self.parse_comma_separated(Parser::parse_expr)?; + } else if self.parse_keyword(Keyword::METRICS) { + if !metrics.is_empty() { + return Err(ParserError::ParserError( + "METRICS clause can only be specified once".to_string(), + )); + } + metrics = self.parse_comma_separated(|parser| parser.parse_object_name(true))?; + } else if self.parse_keyword(Keyword::FACTS) { + if !facts.is_empty() { + return Err(ParserError::ParserError( + "FACTS clause can only be specified once".to_string(), + )); + } + facts = self.parse_comma_separated(Parser::parse_expr)?; + } else if self.parse_keyword(Keyword::WHERE) { + if where_clause.is_some() { + return Err(ParserError::ParserError( + "WHERE clause can only be specified once".to_string(), + )); + } + where_clause = Some(self.parse_expr()?); + } else { + return parser_err!( + "Expected one of DIMENSIONS, METRICS, FACTS or WHERE", + self.peek_token().span.start + )?; + } + } + + self.expect_token(&Token::RParen)?; + + let alias = self.maybe_parse_table_alias()?; + + Ok(TableFactor::SemanticView { + name, + dimensions, + metrics, + facts, + where_clause, + alias, + }) + } + fn parse_match_recognize(&mut self, table: TableFactor) -> Result { self.expect_token(&Token::LParen)?; @@ -12909,7 +14391,13 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let aggregate_functions = self.parse_comma_separated(Self::parse_aliased_function_call)?; self.expect_keyword_is(Keyword::FOR)?; - let value_column = self.parse_period_separated(|p| p.parse_identifier())?; + let value_column = if self.peek_token_ref().token == Token::LParen { + self.parse_parenthesized_column_list_inner(Mandatory, false, |p| { + p.parse_subexpr(self.dialect.prec_value(Precedence::Between)) + })? + } else { + vec![self.parse_subexpr(self.dialect.prec_value(Precedence::Between))?] + }; self.expect_keyword_is(Keyword::IN)?; self.expect_token(&Token::LParen)?; @@ -12953,17 +14441,29 @@ impl<'a> Parser<'a> { &mut self, table: TableFactor, ) -> Result { + let null_inclusion = if self.parse_keyword(Keyword::INCLUDE) { + self.expect_keyword_is(Keyword::NULLS)?; + Some(NullInclusion::IncludeNulls) + } else if self.parse_keyword(Keyword::EXCLUDE) { + self.expect_keyword_is(Keyword::NULLS)?; + Some(NullInclusion::ExcludeNulls) + } else { + None + }; self.expect_token(&Token::LParen)?; - let value = self.parse_identifier()?; + let value = self.parse_expr()?; self.expect_keyword_is(Keyword::FOR)?; let name = self.parse_identifier()?; self.expect_keyword_is(Keyword::IN)?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_column_list_inner(Mandatory, false, |p| { + p.parse_expr_with_alias() + })?; self.expect_token(&Token::RParen)?; let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Unpivot { table: Box::new(table), value, + null_inclusion, name, columns, alias, @@ -12987,7 +14487,7 @@ impl<'a> Parser<'a> { /// Parse a GRANT statement. pub fn parse_grant(&mut self) -> Result { - let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::TO)?; let grantees = self.parse_grantees()?; @@ -12995,16 +14495,35 @@ impl<'a> Parser<'a> { let with_grant_option = self.parse_keywords(&[Keyword::WITH, Keyword::GRANT, Keyword::OPTION]); - let granted_by = self - .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) - .then(|| self.parse_identifier().unwrap()); + let current_grants = + if self.parse_keywords(&[Keyword::COPY, Keyword::CURRENT, Keyword::GRANTS]) { + Some(CurrentGrantsKind::CopyCurrentGrants) + } else if self.parse_keywords(&[Keyword::REVOKE, Keyword::CURRENT, Keyword::GRANTS]) { + Some(CurrentGrantsKind::RevokeCurrentGrants) + } else { + None + }; + + let as_grantor = if self.parse_keywords(&[Keyword::AS]) { + Some(self.parse_identifier()?) + } else { + None + }; + + let granted_by = if self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) { + Some(self.parse_identifier()?) + } else { + None + }; Ok(Statement::Grant { privileges, objects, grantees, with_grant_option, + as_grantor, granted_by, + current_grants, }) } @@ -13012,7 +14531,7 @@ impl<'a> Parser<'a> { let mut values = vec![]; let mut grantee_type = GranteesType::None; loop { - grantee_type = if self.parse_keyword(Keyword::ROLE) { + let new_grantee_type = if self.parse_keyword(Keyword::ROLE) { GranteesType::Role } else if self.parse_keyword(Keyword::USER) { GranteesType::User @@ -13029,9 +14548,19 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::APPLICATION) { GranteesType::Application } else { - grantee_type // keep from previous iteraton, if not specified + grantee_type.clone() // keep from previous iteraton, if not specified }; + if self + .dialect + .get_reserved_grantees_types() + .contains(&new_grantee_type) + { + self.prev_token(); + } else { + grantee_type = new_grantee_type; + } + let grantee = if grantee_type == GranteesType::Public { Grantee { grantee_type: grantee_type.clone(), @@ -13046,7 +14575,7 @@ impl<'a> Parser<'a> { let ident = self.parse_identifier()?; if let GranteeName::ObjectName(namespace) = name { name = GranteeName::ObjectName(ObjectName::from(vec![Ident::new( - format!("{}:{}", namespace, ident), + format!("{namespace}:{ident}"), )])); }; } @@ -13066,7 +14595,7 @@ impl<'a> Parser<'a> { Ok(values) } - pub fn parse_grant_revoke_privileges_objects( + pub fn parse_grant_deny_revoke_privileges_objects( &mut self, ) -> Result<(Privileges, Option), ParserError> { let privileges = if self.parse_keyword(Keyword::ALL) { @@ -13083,6 +14612,91 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllTablesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::EXTERNAL, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllExternalTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::MATERIALIZED, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllMaterializedViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::FUNCTIONS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllFunctionsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::SCHEMAS, + Keyword::IN, + Keyword::DATABASE, + ]) { + Some(GrantObjects::FutureSchemasInDatabase { + databases: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::EXTERNAL, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureExternalTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::MATERIALIZED, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureMaterializedViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::ALL, Keyword::SEQUENCES, @@ -13092,31 +14706,39 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllSequencesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::SEQUENCES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureSequencesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[Keyword::RESOURCE, Keyword::MONITOR]) { - Some(GrantObjects::ResourceMonitors(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::ResourceMonitors( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else if self.parse_keywords(&[Keyword::COMPUTE, Keyword::POOL]) { - Some(GrantObjects::ComputePools(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::ComputePools( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else if self.parse_keywords(&[Keyword::FAILOVER, Keyword::GROUP]) { - Some(GrantObjects::FailoverGroup(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::FailoverGroup( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else if self.parse_keywords(&[Keyword::REPLICATION, Keyword::GROUP]) { - Some(GrantObjects::ReplicationGroup(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::ReplicationGroup( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else if self.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) { - Some(GrantObjects::ExternalVolumes(self.parse_comma_separated( - |p| p.parse_object_name_with_wildcards(false, true), - )?)) + Some(GrantObjects::ExternalVolumes( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else { let object_type = self.parse_one_of_keywords(&[ Keyword::SEQUENCE, Keyword::DATABASE, - Keyword::DATABASE, Keyword::SCHEMA, Keyword::TABLE, Keyword::VIEW, @@ -13127,9 +14749,11 @@ impl<'a> Parser<'a> { Keyword::INTEGRATION, Keyword::USER, Keyword::CONNECTION, + Keyword::PROCEDURE, + Keyword::FUNCTION, ]); let objects = - self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); + self.parse_comma_separated(|p| p.parse_object_name_inner(false, true)); match object_type { Some(Keyword::DATABASE) => Some(GrantObjects::Databases(objects?)), Some(Keyword::SCHEMA) => Some(GrantObjects::Schemas(objects?)), @@ -13139,6 +14763,13 @@ impl<'a> Parser<'a> { Some(Keyword::VIEW) => Some(GrantObjects::Views(objects?)), Some(Keyword::USER) => Some(GrantObjects::Users(objects?)), Some(Keyword::CONNECTION) => Some(GrantObjects::Connections(objects?)), + kw @ (Some(Keyword::PROCEDURE) | Some(Keyword::FUNCTION)) => { + if let Some(name) = objects?.first() { + self.parse_grant_procedure_or_function(name, &kw)? + } else { + self.expected("procedure or function name", self.peek_token())? + } + } Some(Keyword::TABLE) | None => Some(GrantObjects::Tables(objects?)), _ => unreachable!(), } @@ -13150,6 +14781,31 @@ impl<'a> Parser<'a> { Ok((privileges, objects)) } + fn parse_grant_procedure_or_function( + &mut self, + name: &ObjectName, + kw: &Option, + ) -> Result, ParserError> { + let arg_types = if self.consume_token(&Token::LParen) { + let list = self.parse_comma_separated0(Self::parse_data_type, Token::RParen)?; + self.expect_token(&Token::RParen)?; + list + } else { + vec![] + }; + match kw { + Some(Keyword::PROCEDURE) => Ok(Some(GrantObjects::Procedure { + name: name.clone(), + arg_types, + })), + Some(Keyword::FUNCTION) => Ok(Some(GrantObjects::Function { + name: name.clone(), + arg_types, + })), + _ => self.expected("procedure or function keywords", self.peek_token())?, + } + } + pub fn parse_grant_permission(&mut self) -> Result { fn parse_columns(parser: &mut Parser) -> Result>, ParserError> { let columns = parser.parse_parenthesized_column_list(Optional, false)?; @@ -13211,6 +14867,9 @@ impl<'a> Parser<'a> { Ok(Action::Create { obj_type }) } else if self.parse_keyword(Keyword::DELETE) { Ok(Action::Delete) + } else if self.parse_keyword(Keyword::EXEC) { + let obj_type = self.maybe_parse_action_execute_obj_type(); + Ok(Action::Exec { obj_type }) } else if self.parse_keyword(Keyword::EXECUTE) { let obj_type = self.maybe_parse_action_execute_obj_type(); Ok(Action::Execute { obj_type }) @@ -13240,7 +14899,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::REPLICATE) { Ok(Action::Replicate) } else if self.parse_keyword(Keyword::ROLE) { - let role = self.parse_identifier()?; + let role = self.parse_object_name(false)?; Ok(Action::Role { role }) } else if self.parse_keyword(Keyword::SELECT) { Ok(Action::Select { @@ -13260,6 +14919,8 @@ impl<'a> Parser<'a> { Ok(Action::Usage) } else if self.parse_keyword(Keyword::OWNERSHIP) { Ok(Action::Ownership) + } else if self.parse_keyword(Keyword::DROP) { + Ok(Action::Drop) } else { self.expected("a privilege keyword", self.peek_token())? } @@ -13295,6 +14956,8 @@ impl<'a> Parser<'a> { Some(ActionCreateObjectType::Integration) } else if self.parse_keyword(Keyword::ROLE) { Some(ActionCreateObjectType::Role) + } else if self.parse_keyword(Keyword::SCHEMA) { + Some(ActionCreateObjectType::Schema) } else if self.parse_keyword(Keyword::SHARE) { Some(ActionCreateObjectType::Share) } else if self.parse_keyword(Keyword::USER) { @@ -13409,16 +15072,51 @@ impl<'a> Parser<'a> { } } + /// Parse [`Statement::Deny`] + pub fn parse_deny(&mut self) -> Result { + self.expect_keyword(Keyword::DENY)?; + + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; + let objects = match objects { + Some(o) => o, + None => { + return parser_err!( + "DENY statements must specify an object", + self.peek_token().span.start + ) + } + }; + + self.expect_keyword_is(Keyword::TO)?; + let grantees = self.parse_grantees()?; + let cascade = self.parse_cascade_option(); + let granted_by = if self.parse_keywords(&[Keyword::AS]) { + Some(self.parse_identifier()?) + } else { + None + }; + + Ok(Statement::Deny(DenyStatement { + privileges, + objects, + grantees, + cascade, + granted_by, + })) + } + /// Parse a REVOKE statement pub fn parse_revoke(&mut self) -> Result { - let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::FROM)?; let grantees = self.parse_grantees()?; - let granted_by = self - .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) - .then(|| self.parse_identifier().unwrap()); + let granted_by = if self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) { + Some(self.parse_identifier()?) + } else { + None + }; let cascade = self.parse_cascade_option(); @@ -13749,6 +15447,11 @@ impl<'a> Parser<'a> { } else { None }; + let limit = if self.parse_keyword(Keyword::LIMIT) { + Some(self.parse_expr()?) + } else { + None + }; Ok(Statement::Update { table, assignments, @@ -13756,6 +15459,7 @@ impl<'a> Parser<'a> { selection, returning, or, + limit, }) } @@ -13979,7 +15683,7 @@ impl<'a> Parser<'a> { self.dialect .get_reserved_keywords_for_select_item_operator(), ) - .map(|keyword| Ident::new(format!("{:?}", keyword))); + .map(|keyword| Ident::new(format!("{keyword:?}"))); match self.parse_wildcard_expr()? { Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard( @@ -14046,8 +15750,7 @@ impl<'a> Parser<'a> { } else { None }; - let opt_exclude = if opt_ilike.is_none() - && dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) + let opt_exclude = if opt_ilike.is_none() && self.dialect.supports_select_wildcard_exclude() { self.parse_optional_select_item_exclude()? } else { @@ -14399,7 +16102,8 @@ impl<'a> Parser<'a> { /// Parse a FETCH clause pub fn parse_fetch(&mut self) -> Result { - self.expect_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT])?; + let _ = self.parse_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]); + let (quantity, percent) = if self .parse_one_of_keywords(&[Keyword::ROW, Keyword::ROWS]) .is_some() @@ -14408,16 +16112,16 @@ impl<'a> Parser<'a> { } else { let quantity = Expr::Value(self.parse_value()?); let percent = self.parse_keyword(Keyword::PERCENT); - self.expect_one_of_keywords(&[Keyword::ROW, Keyword::ROWS])?; + let _ = self.parse_one_of_keywords(&[Keyword::ROW, Keyword::ROWS]); (Some(quantity), percent) }; + let with_ties = if self.parse_keyword(Keyword::ONLY) { false - } else if self.parse_keywords(&[Keyword::WITH, Keyword::TIES]) { - true } else { - return self.expected("one of ONLY or WITH TIES", self.peek_token()); + self.parse_keywords(&[Keyword::WITH, Keyword::TIES]) }; + Ok(Fetch { with_ties, percent, @@ -14480,7 +16184,7 @@ impl<'a> Parser<'a> { transaction: Some(BeginTransactionKind::Transaction), modifier: None, statements: vec![], - exception_statements: None, + exception: None, has_end_keyword: false, }) } @@ -14512,11 +16216,56 @@ impl<'a> Parser<'a> { transaction, modifier, statements: vec![], - exception_statements: None, + exception: None, has_end_keyword: false, }) } + pub fn parse_begin_exception_end(&mut self) -> Result { + let statements = self.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?; + + let exception = if self.parse_keyword(Keyword::EXCEPTION) { + let mut when = Vec::new(); + + // We can have multiple `WHEN` arms so we consume all cases until `END` + while !self.peek_keyword(Keyword::END) { + self.expect_keyword(Keyword::WHEN)?; + + // Each `WHEN` case can have one or more conditions, e.g. + // WHEN EXCEPTION_1 [OR EXCEPTION_2] THEN + // So we parse identifiers until the `THEN` keyword. + let mut idents = Vec::new(); + + while !self.parse_keyword(Keyword::THEN) { + let ident = self.parse_identifier()?; + idents.push(ident); + + self.maybe_parse(|p| p.expect_keyword(Keyword::OR))?; + } + + let statements = self.parse_statement_list(&[Keyword::WHEN, Keyword::END])?; + + when.push(ExceptionWhen { idents, statements }); + } + + Some(when) + } else { + None + }; + + self.expect_keyword(Keyword::END)?; + + Ok(Statement::StartTransaction { + begin: true, + statements, + exception, + has_end_keyword: true, + transaction: None, + modifier: None, + modes: Default::default(), + }) + } + pub fn parse_end(&mut self) -> Result { let modifier = if !self.dialect.supports_end_transaction_modifier() { None @@ -14667,10 +16416,11 @@ impl<'a> Parser<'a> { let has_parentheses = self.consume_token(&Token::LParen); + let end_kws = &[Keyword::USING, Keyword::OUTPUT, Keyword::DEFAULT]; let end_token = match (has_parentheses, self.peek_token().token) { (true, _) => Token::RParen, (false, Token::EOF) => Token::EOF, - (false, Token::Word(w)) if w.keyword == Keyword::USING => Token::Word(w), + (false, Token::Word(w)) if end_kws.contains(&w.keyword) => Token::Word(w), (false, _) => Token::SemiColon, }; @@ -14692,6 +16442,10 @@ impl<'a> Parser<'a> { vec![] }; + let output = self.parse_keyword(Keyword::OUTPUT); + + let default = self.parse_keyword(Keyword::DEFAULT); + Ok(Statement::Execute { immediate: name.is_none(), name, @@ -14699,6 +16453,8 @@ impl<'a> Parser<'a> { has_parentheses, into, using, + output, + default, }) } @@ -14836,15 +16592,22 @@ impl<'a> Parser<'a> { Ok(clauses) } - fn parse_output(&mut self) -> Result { - self.expect_keyword_is(Keyword::OUTPUT)?; + fn parse_output(&mut self, start_keyword: Keyword) -> Result { let select_items = self.parse_projection()?; - self.expect_keyword_is(Keyword::INTO)?; - let into_table = self.parse_select_into()?; + let into_table = if start_keyword == Keyword::OUTPUT && self.peek_keyword(Keyword::INTO) { + self.expect_keyword_is(Keyword::INTO)?; + Some(self.parse_select_into()?) + } else { + None + }; - Ok(OutputClause { - select_items, - into_table, + Ok(if start_keyword == Keyword::OUTPUT { + OutputClause::Output { + select_items, + into_table, + } + } else { + OutputClause::Returning { select_items } }) } @@ -14874,10 +16637,9 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::ON)?; let on = self.parse_expr()?; let clauses = self.parse_merge_clauses()?; - let output = if self.peek_keyword(Keyword::OUTPUT) { - Some(self.parse_output()?) - } else { - None + let output = match self.parse_one_of_keywords(&[Keyword::OUTPUT, Keyword::RETURNING]) { + Some(start_keyword) => Some(self.parse_output(start_keyword)?), + None => None, }; Ok(Statement::Merge { @@ -15087,6 +16849,49 @@ impl<'a> Parser<'a> { Ok(sequence_options) } + /// Parse a `CREATE SERVER` statement. + /// + /// See [Statement::CreateServer] + pub fn parse_pg_create_server(&mut self) -> Result { + let ine = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + + let server_type = if self.parse_keyword(Keyword::TYPE) { + Some(self.parse_identifier()?) + } else { + None + }; + + let version = if self.parse_keyword(Keyword::VERSION) { + Some(self.parse_identifier()?) + } else { + None + }; + + self.expect_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER])?; + let foreign_data_wrapper = self.parse_object_name(false)?; + + let mut options = None; + if self.parse_keyword(Keyword::OPTIONS) { + self.expect_token(&Token::LParen)?; + options = Some(self.parse_comma_separated(|p| { + let key = p.parse_identifier()?; + let value = p.parse_identifier()?; + Ok(CreateServerOption { key, value }) + })?); + self.expect_token(&Token::RParen)?; + } + + Ok(Statement::CreateServer(CreateServerStatement { + name, + if_not_exists: ine, + server_type, + version, + foreign_data_wrapper, + options, + })) + } + /// The index of the first unprocessed token. pub fn index(&self) -> usize { self.index @@ -15110,22 +16915,30 @@ impl<'a> Parser<'a> { pub fn parse_create_procedure(&mut self, or_alter: bool) -> Result { let name = self.parse_object_name(false)?; let params = self.parse_optional_procedure_parameters()?; + + let language = if self.parse_keyword(Keyword::LANGUAGE) { + Some(self.parse_identifier()?) + } else { + None + }; + self.expect_keyword_is(Keyword::AS)?; - self.expect_keyword_is(Keyword::BEGIN)?; - let statements = self.parse_statements()?; - self.expect_keyword_is(Keyword::END)?; + + let body = self.parse_conditional_statements(&[Keyword::END])?; + Ok(Statement::CreateProcedure { name, or_alter, params, - body: statements, + language, + body, }) } pub fn parse_window_spec(&mut self) -> Result { let window_name = match self.peek_token().token { Token::Word(word) if word.keyword == Keyword::NoKeyword => { - self.parse_optional_indent()? + self.parse_optional_ident()? } _ => None, }; @@ -15216,9 +17029,9 @@ impl<'a> Parser<'a> { fn parse_parenthesized_identifiers(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(|p| p.parse_identifier())?; + let idents = self.parse_comma_separated0(|p| p.parse_identifier(), Token::RParen)?; self.expect_token(&Token::RParen)?; - Ok(partitions) + Ok(idents) } fn parse_column_position(&mut self) -> Result, ParserError> { @@ -15253,6 +17066,64 @@ impl<'a> Parser<'a> { } } + /// /// Parse a `EXPORT DATA` statement. + /// + /// See [Statement::ExportData] + fn parse_export_data(&mut self) -> Result { + self.expect_keywords(&[Keyword::EXPORT, Keyword::DATA])?; + + let connection = if self.parse_keywords(&[Keyword::WITH, Keyword::CONNECTION]) { + Some(self.parse_object_name(false)?) + } else { + None + }; + self.expect_keyword(Keyword::OPTIONS)?; + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(|p| p.parse_sql_option())?; + self.expect_token(&Token::RParen)?; + self.expect_keyword(Keyword::AS)?; + let query = self.parse_query()?; + Ok(Statement::ExportData(ExportData { + options, + query, + connection, + })) + } + + fn parse_vacuum(&mut self) -> Result { + self.expect_keyword(Keyword::VACUUM)?; + let full = self.parse_keyword(Keyword::FULL); + let sort_only = self.parse_keywords(&[Keyword::SORT, Keyword::ONLY]); + let delete_only = self.parse_keywords(&[Keyword::DELETE, Keyword::ONLY]); + let reindex = self.parse_keyword(Keyword::REINDEX); + let recluster = self.parse_keyword(Keyword::RECLUSTER); + let (table_name, threshold, boost) = + match self.maybe_parse(|p| p.parse_object_name(false))? { + Some(table_name) => { + let threshold = if self.parse_keyword(Keyword::TO) { + let value = self.parse_value()?; + self.expect_keyword(Keyword::PERCENT)?; + Some(value.value) + } else { + None + }; + let boost = self.parse_keyword(Keyword::BOOST); + (Some(table_name), threshold, boost) + } + _ => (None, None, false), + }; + Ok(Statement::Vacuum(VacuumStatement { + full, + sort_only, + delete_only, + reindex, + recluster, + table_name, + threshold, + boost, + })) + } + /// Consume the parser and return its underlying token buffer pub fn into_tokens(self) -> Vec { self.tokens @@ -15389,6 +17260,82 @@ impl<'a> Parser<'a> { Ok(None) } } + + pub(crate) fn in_column_definition_state(&self) -> bool { + matches!(self.state, ColumnDefinition) + } + + /// Parses options provided in key-value format. + /// + /// * `parenthesized` - true if the options are enclosed in parenthesis + /// * `end_words` - a list of keywords that any of them indicates the end of the options section + pub(crate) fn parse_key_value_options( + &mut self, + parenthesized: bool, + end_words: &[Keyword], + ) -> Result, ParserError> { + let mut options: Vec = Vec::new(); + if parenthesized { + self.expect_token(&Token::LParen)?; + } + loop { + match self.next_token().token { + Token::RParen => { + if parenthesized { + break; + } else { + return self.expected(" another option or EOF", self.peek_token()); + } + } + Token::EOF => break, + Token::Comma => continue, + Token::Word(w) if !end_words.contains(&w.keyword) => { + options.push(self.parse_key_value_option(w)?) + } + Token::Word(w) if end_words.contains(&w.keyword) => { + self.prev_token(); + break; + } + _ => return self.expected("another option, EOF, Comma or ')'", self.peek_token()), + }; + } + Ok(options) + } + + /// Parses a `KEY = VALUE` construct based on the specified key + pub(crate) fn parse_key_value_option( + &mut self, + key: Word, + ) -> Result { + self.expect_token(&Token::Eq)?; + match self.next_token().token { + Token::SingleQuotedString(value) => Ok(KeyValueOption { + option_name: key.value, + option_type: KeyValueOptionType::STRING, + value, + }), + Token::Word(word) + if word.keyword == Keyword::TRUE || word.keyword == Keyword::FALSE => + { + Ok(KeyValueOption { + option_name: key.value, + option_type: KeyValueOptionType::BOOLEAN, + value: word.value.to_uppercase(), + }) + } + Token::Word(word) => Ok(KeyValueOption { + option_name: key.value, + option_type: KeyValueOptionType::ENUM, + value: word.value, + }), + Token::Number(n, _) => Ok(KeyValueOption { + option_name: key.value, + option_type: KeyValueOptionType::NUMBER, + value: n, + }), + _ => self.expected("expected option value", self.peek_token()), + } + } } fn maybe_prefixed_expr(expr: Expr, prefix: Option) -> Expr { @@ -15499,7 +17446,7 @@ mod tests { use crate::ast::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, ObjectName, TimezoneInfo, }; - use crate::dialect::{AnsiDialect, GenericDialect}; + use crate::dialect::{AnsiDialect, GenericDialect, PostgreSqlDialect}; use crate::test_utils::TestedDialects; macro_rules! test_parse_data_type { @@ -15705,8 +17652,11 @@ mod tests { #[test] fn test_ansii_exact_numeric_types() { // Exact numeric types: - let dialect = - TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})]); + let dialect = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(AnsiDialect {}), + Box::new(PostgreSqlDialect {}), + ]); test_parse_data_type!(dialect, "NUMERIC", DataType::Numeric(ExactNumberInfo::None)); @@ -15749,6 +17699,53 @@ mod tests { "DEC(2,10)", DataType::Dec(ExactNumberInfo::PrecisionAndScale(2, 10)) ); + + // Test negative scale values. + test_parse_data_type!( + dialect, + "NUMERIC(10,-2)", + DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, -2)) + ); + + test_parse_data_type!( + dialect, + "DECIMAL(1000,-10)", + DataType::Decimal(ExactNumberInfo::PrecisionAndScale(1000, -10)) + ); + + test_parse_data_type!( + dialect, + "DEC(5,-1000)", + DataType::Dec(ExactNumberInfo::PrecisionAndScale(5, -1000)) + ); + + test_parse_data_type!( + dialect, + "NUMERIC(10,-5)", + DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, -5)) + ); + + test_parse_data_type!( + dialect, + "DECIMAL(20,-10)", + DataType::Decimal(ExactNumberInfo::PrecisionAndScale(20, -10)) + ); + + test_parse_data_type!( + dialect, + "DEC(5,-2)", + DataType::Dec(ExactNumberInfo::PrecisionAndScale(5, -2)) + ); + + dialect.run_parser_method("NUMERIC(10,+5)", |parser| { + let data_type = parser.parse_data_type().unwrap(); + assert_eq!( + DataType::Numeric(ExactNumberInfo::PrecisionAndScale(10, 5)), + data_type + ); + // Note: Explicit '+' sign is not preserved in output, which is correct + assert_eq!("NUMERIC(10,5)", data_type.to_string()); + }); } #[test] @@ -15864,6 +17861,20 @@ mod tests { }}; } + fn mk_expected_col(name: &str) -> IndexColumn { + IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(name.into()), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: None, + } + } + let dialect = TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})]); @@ -15874,7 +17885,8 @@ mod tests { display_as_key: false, name: None, index_type: None, - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], + index_options: vec![], } ); @@ -15885,7 +17897,8 @@ mod tests { display_as_key: true, name: None, index_type: None, - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], + index_options: vec![], } ); @@ -15896,7 +17909,8 @@ mod tests { display_as_key: false, name: Some(Ident::with_quote('\'', "index")), index_type: None, - columns: vec![Ident::new("c1"), Ident::new("c2")], + columns: vec![mk_expected_col("c1"), mk_expected_col("c2")], + index_options: vec![], } ); @@ -15907,7 +17921,8 @@ mod tests { display_as_key: false, name: None, index_type: Some(IndexType::BTree), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], + index_options: vec![], } ); @@ -15918,7 +17933,8 @@ mod tests { display_as_key: false, name: None, index_type: Some(IndexType::Hash), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], + index_options: vec![], } ); @@ -15929,7 +17945,8 @@ mod tests { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::BTree), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], + index_options: vec![], } ); @@ -15940,7 +17957,8 @@ mod tests { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::Hash), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], + index_options: vec![], } ); } @@ -16110,4 +18128,12 @@ mod tests { assert!(Parser::parse_sql(&MySqlDialect {}, sql).is_err()); } + + #[test] + fn test_placeholder_invalid_whitespace() { + for w in [" ", "/*invalid*/"] { + let sql = format!("\nSELECT\n :{w}fooBar"); + assert!(Parser::parse_sql(&GenericDialect, &sql).is_err()); + } + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 6270ac42b..ab2cf89b2 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -151,6 +151,8 @@ impl TestedDialects { /// /// 2. re-serializing the result of parsing `sql` produces the same /// `canonical` sql string + /// + /// For multiple statements, use [`statements_parse_to`]. pub fn one_statement_parses_to(&self, sql: &str, canonical: &str) -> Statement { let mut statements = self.parse_sql_statements(sql).expect(sql); assert_eq!(statements.len(), 1); @@ -166,6 +168,24 @@ impl TestedDialects { only_statement } + /// The same as [`one_statement_parses_to`] but it works for a multiple statements + pub fn statements_parse_to(&self, sql: &str, canonical: &str) -> Vec { + let statements = self.parse_sql_statements(sql).expect(sql); + if !canonical.is_empty() && sql != canonical { + assert_eq!(self.parse_sql_statements(canonical).unwrap(), statements); + } else { + assert_eq!( + sql, + statements + .iter() + .map(|s| s.to_string()) + .collect::>() + .join("; ") + ); + } + statements + } + /// Ensures that `sql` parses as an [`Expr`], and that /// re-serializing the parse result produces canonical pub fn expr_parses_to(&self, sql: &str, canonical: &str) -> Expr { @@ -250,7 +270,7 @@ impl TestedDialects { tokenizer = tokenizer.with_unescape(options.unescape); } let tokens = tokenizer.tokenize().unwrap(); - assert_eq!(expected, tokens, "Tokenized differently for {:?}", dialect); + assert_eq!(expected, tokens, "Tokenized differently for {dialect:?}"); }); } } @@ -274,6 +294,11 @@ pub fn all_dialects() -> TestedDialects { ]) } +// Returns all available dialects with the specified parser options +pub fn all_dialects_with_options(options: ParserOptions) -> TestedDialects { + TestedDialects::new_with_options(all_dialects().dialects, options) +} + /// Returns all dialects matching the given predicate. pub fn all_dialects_where(predicate: F) -> TestedDialects where @@ -325,10 +350,13 @@ pub fn alter_table_op_with_name(stmt: Statement, expected_name: &str) -> AlterTa operations, on_cluster: _, location: _, + iceberg, + end_token: _, } => { assert_eq!(name.to_string(), expected_name); assert!(!if_exists); assert!(!is_only); + assert!(!iceberg); only(operations) } _ => panic!("Expected ALTER TABLE statement"), @@ -344,6 +372,11 @@ pub fn number(n: &str) -> Value { Value::Number(n.parse().unwrap(), false) } +/// Creates a [Value::SingleQuotedString] +pub fn single_quoted_string(s: impl Into) -> Value { + Value::SingleQuotedString(s.into()) +} + pub fn table_alias(name: impl Into) -> Option { Some(TableAlias { name: Ident::new(name), @@ -426,3 +459,52 @@ pub fn call(function: &str, args: impl IntoIterator) -> Expr { within_group: vec![], }) } + +/// Gets the first index column (mysql calls it a key part) of the first index found in a +/// [`Statement::CreateIndex`], [`Statement::CreateTable`], or [`Statement::AlterTable`]. +pub fn index_column(stmt: Statement) -> Expr { + match stmt { + Statement::CreateIndex(CreateIndex { columns, .. }) => { + columns.first().unwrap().column.expr.clone() + } + Statement::CreateTable(CreateTable { constraints, .. }) => { + match constraints.first().unwrap() { + TableConstraint::Index { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::Unique { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::PrimaryKey { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::FulltextOrSpatial { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + } + } + Statement::AlterTable { operations, .. } => match operations.first().unwrap() { + AlterTableOperation::AddConstraint { constraint, .. } => { + match constraint { + TableConstraint::Index { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::Unique { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::PrimaryKey { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::FulltextOrSpatial { + columns, + .. + } => columns.first().unwrap().column.expr.clone(), + _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + } + } + _ => panic!("Expected a constraint"), + }, + _ => panic!("Expected CREATE INDEX, ALTER TABLE, or CREATE TABLE, got: {stmt:?}"), + } +} diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 13bce0c0d..8382a5344 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -246,6 +246,8 @@ pub enum Token { ShiftLeftVerticalBar, /// `|>> PostgreSQL/Redshift geometrical binary operator (Is strictly above?) VerticalBarShiftRight, + /// `|> BigQuery pipe operator + VerticalBarRightAngleBracket, /// `#>>`, extracts JSON sub-object at the specified path as text HashLongArrow, /// jsonb @> jsonb -> boolean: Test whether left json contains the right json @@ -359,6 +361,7 @@ impl fmt::Display for Token { Token::AmpersandRightAngleBracket => f.write_str("&>"), Token::AmpersandLeftAngleBracketVerticalBar => f.write_str("&<|"), Token::VerticalBarAmpersandRightAngleBracket => f.write_str("|&>"), + Token::VerticalBarRightAngleBracket => f.write_str("|>"), Token::TwoWayArrow => f.write_str("<->"), Token::LeftAngleBracketCaret => f.write_str("<^"), Token::RightAngleBracketCaret => f.write_str(">^"), @@ -1188,6 +1191,22 @@ impl<'a> Tokenizer<'a> { } // numbers and period '0'..='9' | '.' => { + // special case where if ._ is encountered after a word then that word + // is a table and the _ is the start of the col name. + // if the prev token is not a word, then this is not a valid sql + // word or number. + if ch == '.' && chars.peekable.clone().nth(1) == Some('_') { + if let Some(Token::Word(_)) = prev_token { + chars.next(); + return Ok(Some(Token::Period)); + } + + return self.tokenizer_error( + chars.location(), + "Unexpected character '_'".to_string(), + ); + } + // Some dialects support underscore as number separator // There can only be one at a time and it must be followed by another digit let is_number_separator = |ch: char, next_char: Option| { @@ -1403,6 +1422,9 @@ impl<'a> Tokenizer<'a> { _ => self.start_binop_opt(chars, "|>", None), } } + Some('>') if self.dialect.supports_pipe_operator() => { + self.consume_for_binop(chars, "|>", Token::VerticalBarRightAngleBracket) + } // Bitshift '|' operator _ => self.start_binop(chars, "|", Token::Pipe), } @@ -1729,7 +1751,7 @@ impl<'a> Tokenizer<'a> { (None, Some(tok)) => Ok(Some(tok)), (None, None) => self.tokenizer_error( chars.location(), - format!("Expected a valid binary operator after '{}'", prefix), + format!("Expected a valid binary operator after '{prefix}'"), ), } } @@ -1787,7 +1809,7 @@ impl<'a> Tokenizer<'a> { chars.next(); let mut temp = String::new(); - let end_delimiter = format!("${}$", value); + let end_delimiter = format!("${value}$"); loop { match chars.next() { @@ -2380,13 +2402,13 @@ fn take_char_from_hex_digits( location: chars.location(), })?; let digit = next_char.to_digit(16).ok_or_else(|| TokenizerError { - message: format!("Invalid hex digit in escaped unicode string: {}", next_char), + message: format!("Invalid hex digit in escaped unicode string: {next_char}"), location: chars.location(), })?; result = result * 16 + digit; } char::from_u32(result).ok_or_else(|| TokenizerError { - message: format!("Invalid unicode character: {:x}", result), + message: format!("Invalid unicode character: {result:x}"), location: chars.location(), }) } @@ -3482,7 +3504,7 @@ mod tests { } fn check_unescape(s: &str, expected: Option<&str>) { - let s = format!("'{}'", s); + let s = format!("'{s}'"); let mut state = State { peekable: s.chars().peekable(), line: 0, @@ -4012,4 +4034,40 @@ mod tests { ], ); } + + #[test] + fn tokenize_period_underscore() { + let sql = String::from("SELECT table._col"); + // a dialect that supports underscores in numeric literals + let dialect = PostgreSqlDialect {}; + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Word(Word { + value: "table".to_string(), + quote_style: None, + keyword: Keyword::TABLE, + }), + Token::Period, + Token::Word(Word { + value: "_col".to_string(), + quote_style: None, + keyword: Keyword::NoKeyword, + }), + ]; + + compare(expected, tokens); + + let sql = String::from("SELECT ._123"); + if let Ok(tokens) = Tokenizer::new(&dialect, &sql).tokenize() { + panic!("Tokenizer should have failed on {sql}, but it succeeded with {tokens:?}"); + } + + let sql = String::from("SELECT ._abc"); + if let Ok(tokens) = Tokenizer::new(&dialect, &sql).tokenize() { + panic!("Tokenizer should have failed on {sql}, but it succeeded with {tokens:?}"); + } + } } diff --git a/tests/pretty_print.rs b/tests/pretty_print.rs new file mode 100644 index 000000000..f5a9d8613 --- /dev/null +++ b/tests/pretty_print.rs @@ -0,0 +1,414 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 sqlparser::dialect::GenericDialect; +use sqlparser::parser::Parser; + +fn prettify(sql: &str) -> String { + let ast = Parser::parse_sql(&GenericDialect {}, sql).unwrap(); + format!("{:#}", ast[0]) +} + +#[test] +fn test_pretty_print_select() { + assert_eq!( + prettify("SELECT a, b, c FROM my_table WHERE x = 1 AND y = 2"), + r#" +SELECT + a, + b, + c +FROM + my_table +WHERE + x = 1 AND y = 2 +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_join() { + assert_eq!( + prettify("SELECT a FROM table1 JOIN table2 ON table1.id = table2.id"), + r#" +SELECT + a +FROM + table1 + JOIN table2 ON table1.id = table2.id +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_subquery() { + assert_eq!( + prettify("SELECT * FROM (SELECT a, b FROM my_table) AS subquery"), + r#" +SELECT + * +FROM + ( + SELECT + a, + b + FROM + my_table + ) AS subquery +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_union() { + assert_eq!( + prettify("SELECT a FROM table1 UNION SELECT b FROM table2"), + r#" +SELECT + a +FROM + table1 +UNION +SELECT + b +FROM + table2 +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_group_by() { + assert_eq!( + prettify("SELECT a, COUNT(*) FROM my_table GROUP BY a HAVING COUNT(*) > 1"), + r#" +SELECT + a, + COUNT(*) +FROM + my_table +GROUP BY + a +HAVING + COUNT(*) > 1 +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_cte() { + assert_eq!( + prettify("WITH cte AS (SELECT a, b FROM my_table) SELECT * FROM cte"), + r#" +WITH cte AS ( + SELECT + a, + b + FROM + my_table +) +SELECT + * +FROM + cte +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_case_when() { + assert_eq!( + prettify("SELECT CASE WHEN x > 0 THEN 'positive' WHEN x < 0 THEN 'negative' ELSE 'zero' END FROM my_table"), + r#" +SELECT + CASE + WHEN x > 0 THEN + 'positive' + WHEN x < 0 THEN + 'negative' + ELSE + 'zero' + END +FROM + my_table +"#.trim() + ); +} + +#[test] +fn test_pretty_print_window_function() { + assert_eq!( + prettify("SELECT id, value, ROW_NUMBER() OVER (PARTITION BY category ORDER BY value DESC) as rank FROM my_table"), + r#" +SELECT + id, + value, + ROW_NUMBER() OVER ( + PARTITION BY category + ORDER BY value DESC + ) AS rank +FROM + my_table +"#.trim() + ); +} + +#[test] +fn test_pretty_print_multiline_string() { + assert_eq!( + prettify("SELECT 'multiline\nstring' AS str"), + r#" +SELECT + 'multiline +string' AS str +"# + .trim(), + "A literal string with a newline should be kept as is. The contents of the string should not be indented." + ); +} + +#[test] +fn test_pretty_print_insert_values() { + assert_eq!( + prettify("INSERT INTO my_table (a, b, c) VALUES (1, 2, 3), (4, 5, 6)"), + r#" +INSERT INTO my_table (a, b, c) +VALUES + (1, 2, 3), + (4, 5, 6) +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_insert_select() { + assert_eq!( + prettify("INSERT INTO my_table (a, b) SELECT x, y FROM source_table RETURNING a AS id"), + r#" +INSERT INTO my_table (a, b) +SELECT + x, + y +FROM + source_table +RETURNING + a AS id +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_update() { + assert_eq!( + prettify("UPDATE my_table SET a = 1, b = 2 WHERE x > 0 RETURNING id, name"), + r#" +UPDATE my_table +SET + a = 1, + b = 2 +WHERE + x > 0 +RETURNING + id, + name +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_delete() { + assert_eq!( + prettify("DELETE FROM my_table WHERE x > 0 RETURNING id, name"), + r#" +DELETE FROM + my_table +WHERE + x > 0 +RETURNING + id, + name +"# + .trim() + ); + + assert_eq!( + prettify("DELETE table1, table2"), + r#" +DELETE + table1, + table2 +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_create_table() { + assert_eq!( + prettify("CREATE TABLE my_table (id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, CONSTRAINT fk_other FOREIGN KEY (id) REFERENCES other_table(id))"), + r#" +CREATE TABLE my_table ( + id INT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + CONSTRAINT fk_other FOREIGN KEY (id) REFERENCES other_table(id) +) +"# + .trim() + ); +} + +#[test] +fn test_pretty_print_create_view() { + assert_eq!( + prettify("CREATE VIEW my_view AS SELECT a, b FROM my_table WHERE x > 0"), + r#" +CREATE VIEW my_view AS +SELECT + a, + b +FROM + my_table +WHERE + x > 0 +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_create_function() { + assert_eq!( + prettify("CREATE FUNCTION my_func() RETURNS INT BEGIN SELECT COUNT(*) INTO @count FROM my_table; RETURN @count; END"), + r#" +CREATE FUNCTION my_func() RETURNS INT +BEGIN + SELECT COUNT(*) INTO @count FROM my_table; + RETURN @count; +END +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_json_table() { + assert_eq!( + prettify("SELECT * FROM JSON_TABLE(@json, '$[*]' COLUMNS (id INT PATH '$.id', name VARCHAR(255) PATH '$.name')) AS jt"), + r#" +SELECT + * +FROM + JSON_TABLE( + @json, + '$[*]' COLUMNS ( + id INT PATH '$.id', + name VARCHAR(255) PATH '$.name' + ) + ) AS jt +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_transaction_blocks() { + assert_eq!( + prettify("BEGIN; UPDATE my_table SET x = 1; COMMIT;"), + r#" +BEGIN; +UPDATE my_table SET x = 1; +COMMIT; +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_control_flow() { + assert_eq!( + prettify("IF x > 0 THEN SELECT 'positive'; ELSE SELECT 'negative'; END IF;"), + r#" +IF x > 0 THEN + SELECT 'positive'; +ELSE + SELECT 'negative'; +END IF; +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_merge() { + assert_eq!( + prettify("MERGE INTO target_table t USING source_table s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.value = s.value WHEN NOT MATCHED THEN INSERT (id, value) VALUES (s.id, s.value)"), + r#" +MERGE INTO target_table t +USING source_table s ON t.id = s.id +WHEN MATCHED THEN + UPDATE SET t.value = s.value +WHEN NOT MATCHED THEN + INSERT (id, value) VALUES (s.id, s.value) +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_create_index() { + assert_eq!( + prettify("CREATE INDEX idx_name ON my_table (column1, column2)"), + r#" +CREATE INDEX idx_name +ON my_table (column1, column2) +"# + .trim() + ); +} + +#[test] +#[ignore = "https://github.com/apache/datafusion-sqlparser-rs/issues/1850"] +fn test_pretty_print_explain() { + assert_eq!( + prettify("EXPLAIN ANALYZE SELECT * FROM my_table WHERE x > 0"), + r#" +EXPLAIN ANALYZE +SELECT + * +FROM + my_table +WHERE + x > 0 +"# + .trim() + ); +} diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 416d2e435..4f0cfa3e8 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -20,10 +20,12 @@ mod test_utils; use std::ops::Deref; +use sqlparser::ast::helpers::attached_token::AttachedToken; use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; +use sqlparser::keywords::Keyword; use sqlparser::parser::{ParserError, ParserOptions}; -use sqlparser::tokenizer::{Location, Span}; +use sqlparser::tokenizer::{Location, Span, Token, TokenWithSpan, Word}; use test_utils::*; #[test] @@ -261,10 +263,10 @@ fn parse_at_at_identifier() { #[test] fn parse_begin() { - let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; END"#; + let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; RAISE USING MESSAGE = FORMAT('ERR: %s', 'Bad'); END"#; let Statement::StartTransaction { statements, - exception_statements, + exception, has_end_keyword, .. } = bigquery().verified_stmt(sql) @@ -272,7 +274,10 @@ fn parse_begin() { unreachable!(); }; assert_eq!(1, statements.len()); - assert_eq!(1, exception_statements.unwrap().len()); + assert!(exception.is_some()); + + let exception = exception.unwrap(); + assert_eq!(1, exception.len()); assert!(has_end_keyword); bigquery().verified_stmt( @@ -352,14 +357,16 @@ fn parse_create_view_with_options() { ViewColumnDef { name: Ident::new("age"), data_type: None, - options: Some(vec![ColumnOption::Options(vec![SqlOption::KeyValue { - key: Ident::new("description"), - value: Expr::Value( - Value::DoubleQuotedString("field age".to_string()).with_span( - Span::new(Location::new(1, 42), Location::new(1, 52)) - ) - ), - }])]), + options: Some(ColumnOptions::CommaSeparated(vec![ColumnOption::Options( + vec![SqlOption::KeyValue { + key: Ident::new("description"), + value: Expr::Value( + Value::DoubleQuotedString("field age".to_string()).with_span( + Span::new(Location::new(1, 42), Location::new(1, 52)) + ) + ), + }] + )])), }, ], columns @@ -484,7 +491,7 @@ fn parse_create_table_with_options() { columns, partition_by, cluster_by, - options, + table_options, .. }) => { assert_eq!( @@ -536,10 +543,10 @@ fn parse_create_table_with_options() { ( Some(Box::new(Expr::Identifier(Ident::new("_PARTITIONDATE")))), Some(WrappedCollection::NoWrapping(vec![ - Ident::new("userid"), - Ident::new("age"), + Expr::Identifier(Ident::new("userid")), + Expr::Identifier(Ident::new("age")), ])), - Some(vec![ + CreateTableOptions::Options(vec![ SqlOption::KeyValue { key: Ident::new("partition_expiration_days"), value: Expr::Value( @@ -561,7 +568,7 @@ fn parse_create_table_with_options() { }, ]) ), - (partition_by, cluster_by, options) + (partition_by, cluster_by, table_options) ) } _ => unreachable!(), @@ -601,11 +608,13 @@ fn parse_nested_data_types() { field_name: Some("a".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket( Box::new(DataType::Int64,) - )) + )), + options: None, }, StructField { field_name: Some("b".into()), - field_type: DataType::Bytes(Some(42)) + field_type: DataType::Bytes(Some(42)), + options: None, }, ], StructBracketKind::AngleBrackets @@ -619,6 +628,7 @@ fn parse_nested_data_types() { vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }], StructBracketKind::AngleBrackets ), @@ -632,35 +642,6 @@ fn parse_nested_data_types() { } } -#[test] -fn parse_invalid_brackets() { - let sql = "SELECT STRUCT>(NULL)"; - assert_eq!( - bigquery_and_generic() - .parse_sql_statements(sql) - .unwrap_err(), - ParserError::ParserError("unmatched > in STRUCT literal".to_string()) - ); - - let sql = "SELECT STRUCT>>(NULL)"; - assert_eq!( - bigquery_and_generic() - .parse_sql_statements(sql) - .unwrap_err(), - ParserError::ParserError("Expected: (, found: >".to_string()) - ); - - let sql = "CREATE TABLE table (x STRUCT>>)"; - assert_eq!( - bigquery_and_generic() - .parse_sql_statements(sql) - .unwrap_err(), - ParserError::ParserError( - "Expected: ',' or ')' after column definition, found: >".to_string() - ) - ); -} - #[test] fn parse_tuple_struct_literal() { // tuple syntax: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#tuple_syntax @@ -771,6 +752,7 @@ fn parse_typed_struct_syntax_bigquery() { fields: vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -799,7 +781,8 @@ fn parse_typed_struct_syntax_bigquery() { quote_style: None, span: Span::empty(), }), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident { @@ -807,7 +790,8 @@ fn parse_typed_struct_syntax_bigquery() { quote_style: None, span: Span::empty(), }), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }, ] }, @@ -825,17 +809,20 @@ fn parse_typed_struct_syntax_bigquery() { field_name: Some("arr".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( DataType::Float64 - ))) + ))), + options: None, }, StructField { field_name: Some("str".into()), field_type: DataType::Struct( vec![StructField { field_name: None, - field_type: DataType::Bool + field_type: DataType::Bool, + options: None, }], StructBracketKind::AngleBrackets - ) + ), + options: None, }, ] }, @@ -858,13 +845,15 @@ fn parse_typed_struct_syntax_bigquery() { field_type: DataType::Struct( Default::default(), StructBracketKind::AngleBrackets - ) + ), + options: None, }, StructField { field_name: Some("y".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( DataType::Struct(Default::default(), StructBracketKind::AngleBrackets) - ))) + ))), + options: None, }, ] }, @@ -879,7 +868,8 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::Value(Value::Boolean(true).with_empty_span())], fields: vec![StructField { field_name: None, - field_type: DataType::Bool + field_type: DataType::Bool, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -891,7 +881,8 @@ fn parse_typed_struct_syntax_bigquery() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Bytes(Some(42)) + field_type: DataType::Bytes(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -907,20 +898,26 @@ fn parse_typed_struct_syntax_bigquery() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Date + field_type: DataType::Date, + options: None, }] }, expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::Datetime(None), - value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::Datetime(None) + field_type: DataType::Datetime(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -930,7 +927,8 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::value(number("5.0"))], fields: vec![StructField { field_name: None, - field_type: DataType::Float64 + field_type: DataType::Float64, + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -940,7 +938,8 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::value(number("1"))], fields: vec![StructField { field_name: None, - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[3]) @@ -962,22 +961,31 @@ fn parse_typed_struct_syntax_bigquery() { })], fields: vec![StructField { field_name: None, - field_type: DataType::Interval + field_type: DataType::Interval { + fields: None, + precision: None + }, + options: None, }] }, expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString( - r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() - ) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() + ), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::JSON + field_type: DataType::JSON, + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -993,20 +1001,28 @@ fn parse_typed_struct_syntax_bigquery() { )], fields: vec![StructField { field_name: None, - field_type: DataType::String(Some(42)) + field_type: DataType::String(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString( + "2008-12-25 15:30:00 America/Los_Angeles".into() + ), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::Timestamp(None, TimezoneInfo::None) + field_type: DataType::Timestamp(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1014,13 +1030,18 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: Value::SingleQuotedString("15:30:00".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString("15:30:00".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::Time(None, TimezoneInfo::None) + field_type: DataType::Time(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -1031,26 +1052,36 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::Numeric(ExactNumberInfo::None) + field_type: DataType::Numeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::BigNumeric(ExactNumberInfo::None) + field_type: DataType::BigNumeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1067,10 +1098,12 @@ fn parse_typed_struct_syntax_bigquery() { StructField { field_name: Some("key".into()), field_type: DataType::Int64, + options: None, }, StructField { field_name: Some("value".into()), field_type: DataType::Int64, + options: None, }, ] }, @@ -1092,6 +1125,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { fields: vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1120,7 +1154,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { quote_style: None, span: Span::empty(), }), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident { @@ -1128,7 +1163,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { quote_style: None, span: Span::empty(), }), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }, ] }, @@ -1151,13 +1187,15 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { field_type: DataType::Struct( Default::default(), StructBracketKind::AngleBrackets - ) + ), + options: None, }, StructField { field_name: Some("y".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( DataType::Struct(Default::default(), StructBracketKind::AngleBrackets) - ))) + ))), + options: None, }, ] }, @@ -1172,7 +1210,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::Value(Value::Boolean(true).with_empty_span())], fields: vec![StructField { field_name: None, - field_type: DataType::Bool + field_type: DataType::Bool, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1184,7 +1223,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Bytes(Some(42)) + field_type: DataType::Bytes(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1200,20 +1240,26 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Date + field_type: DataType::Date, + options: None, }] }, expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::Datetime(None), - value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::Datetime(None) + field_type: DataType::Datetime(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1223,7 +1269,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::value(number("5.0"))], fields: vec![StructField { field_name: None, - field_type: DataType::Float64 + field_type: DataType::Float64, + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -1233,7 +1280,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::value(number("1"))], fields: vec![StructField { field_name: None, - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[3]) @@ -1255,22 +1303,31 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { })], fields: vec![StructField { field_name: None, - field_type: DataType::Interval + field_type: DataType::Interval { + fields: None, + precision: None + }, + options: None, }] }, expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString( - r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() - ) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() + ), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::JSON + field_type: DataType::JSON, + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1286,20 +1343,28 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { )], fields: vec![StructField { field_name: None, - field_type: DataType::String(Some(42)) + field_type: DataType::String(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString( + "2008-12-25 15:30:00 America/Los_Angeles".into() + ), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::Timestamp(None, TimezoneInfo::None) + field_type: DataType::Timestamp(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1307,13 +1372,18 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: Value::SingleQuotedString("15:30:00".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString("15:30:00".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::Time(None, TimezoneInfo::None) + field_type: DataType::Time(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -1324,26 +1394,36 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::Numeric(ExactNumberInfo::None) + field_type: DataType::Numeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::TypedString { + values: vec![Expr::TypedString(TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) - }], + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + })], fields: vec![StructField { field_name: None, - field_type: DataType::BigNumeric(ExactNumberInfo::None) + field_type: DataType::BigNumeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1360,7 +1440,8 @@ fn parse_typed_struct_with_field_name_bigquery() { values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1372,7 +1453,8 @@ fn parse_typed_struct_with_field_name_bigquery() { )], fields: vec![StructField { field_name: Some(Ident::from("y")), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1387,11 +1469,13 @@ fn parse_typed_struct_with_field_name_bigquery() { fields: vec![ StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident::from("y")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, } ] }, @@ -1409,7 +1493,8 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1421,7 +1506,8 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { )], fields: vec![StructField { field_name: Some(Ident::from("y")), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1436,11 +1522,13 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { fields: vec![ StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident::from("y")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, } ] }, @@ -2313,16 +2401,46 @@ fn bigquery_select_expr_star() { #[test] fn test_select_as_struct() { - bigquery().verified_only_select("SELECT * FROM (SELECT AS VALUE STRUCT(123 AS a, false AS b))"); + for (sql, parse_to) in [ + ( + "SELECT * FROM (SELECT AS STRUCT STRUCT(123 AS a, false AS b))", + "SELECT * FROM (SELECT AS STRUCT STRUCT(123 AS a, false AS b))", + ), + ( + "SELECT * FROM (SELECT DISTINCT AS STRUCT STRUCT(123 AS a, false AS b))", + "SELECT * FROM (SELECT DISTINCT AS STRUCT STRUCT(123 AS a, false AS b))", + ), + ( + "SELECT * FROM (SELECT ALL AS STRUCT STRUCT(123 AS a, false AS b))", + "SELECT * FROM (SELECT AS STRUCT STRUCT(123 AS a, false AS b))", + ), + ] { + bigquery().one_statement_parses_to(sql, parse_to); + } + let select = bigquery().verified_only_select("SELECT AS STRUCT 1 AS a, 2 AS b"); assert_eq!(Some(ValueTableMode::AsStruct), select.value_table_mode); } #[test] fn test_select_as_value() { - bigquery().verified_only_select( - "SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", - ); + for (sql, parse_to) in [ + ( + "SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + "SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + ), + ( + "SELECT * FROM (SELECT DISTINCT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + "SELECT * FROM (SELECT DISTINCT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + ), + ( + "SELECT * FROM (SELECT ALL AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + "SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))", + ), + ] { + bigquery().one_statement_parses_to(sql, parse_to); + } + let select = bigquery().verified_only_select("SELECT AS VALUE STRUCT(1 AS a, 2 AS b) AS xyz"); assert_eq!(Some(ValueTableMode::AsValue), select.value_table_mode); } @@ -2333,10 +2451,14 @@ fn test_triple_quote_typed_strings() { let expr = bigquery().verified_expr(r#"JSON """{"foo":"bar's"}""""#); assert_eq!( - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::JSON, - value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()) - }, + value: ValueWithSpan { + value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr ); } @@ -2377,3 +2499,341 @@ fn test_any_type() { fn test_any_type_dont_break_custom_type() { bigquery_and_generic().verified_stmt("CREATE TABLE foo (x ANY)"); } + +#[test] +fn test_struct_field_options() { + bigquery().verified_stmt(concat!( + "CREATE TABLE my_table (", + "f0 STRUCT, ", + "f1 STRUCT<", + "a STRING OPTIONS(description = 'This is a string', type = 'string'), ", + "b INT64", + "> OPTIONS(description = 'This is a struct field')", + ")", + )); +} + +#[test] +fn test_struct_trailing_and_nested_bracket() { + bigquery().verified_stmt(concat!( + "CREATE TABLE my_table (", + "f0 STRING, ", + "f1 STRUCT>, ", + "f2 STRING", + ")", + )); + + // More complex nested structs + bigquery().verified_stmt(concat!( + "CREATE TABLE my_table (", + "f0 STRING, ", + "f1 STRUCT>>, ", + "f2 STRUCT>>>, ", + "f3 STRUCT>", + ")", + )); + + // Bad case with missing closing bracket + assert_eq!( + ParserError::ParserError("Expected: >, found: )".to_owned()), + bigquery() + .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT after parsing data type STRUCT)".to_owned() + ), + bigquery() + .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT>)") + .unwrap_err() + ); + + // Base case with redundant closing bracket in nested struct + assert_eq!( + ParserError::ParserError( + "Expected: ',' or ')' after column definition, found: >".to_owned() + ), + bigquery() + .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT>>, c INT64)") + .unwrap_err() + ); + + let sql = "SELECT STRUCT>(NULL)"; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError("unmatched > in STRUCT literal".to_string()) + ); + + let sql = "SELECT STRUCT>>(NULL)"; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError("Expected: (, found: >".to_string()) + ); + + let sql = "CREATE TABLE table (x STRUCT>>)"; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError( + "Expected: ',' or ')' after column definition, found: >".to_string() + ) + ); +} + +#[test] +fn test_export_data() { + let stmt = bigquery().verified_stmt(concat!( + "EXPORT DATA OPTIONS(", + "uri = 'gs://bucket/folder/*', ", + "format = 'PARQUET', ", + "overwrite = true", + ") AS ", + "SELECT field1, field2 FROM mydataset.table1 ORDER BY field1 LIMIT 10", + )); + assert_eq!( + stmt, + Statement::ExportData(ExportData { + options: vec![ + SqlOption::KeyValue { + key: Ident::new("uri"), + value: Expr::Value( + Value::SingleQuotedString("gs://bucket/folder/*".to_owned()) + .with_empty_span() + ), + }, + SqlOption::KeyValue { + key: Ident::new("format"), + value: Expr::Value( + Value::SingleQuotedString("PARQUET".to_owned()).with_empty_span() + ), + }, + SqlOption::KeyValue { + key: Ident::new("overwrite"), + value: Expr::Value(Value::Boolean(true).with_empty_span()), + }, + ], + connection: None, + query: Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken(TokenWithSpan::new( + Token::Word(Word { + value: "SELECT".to_string(), + quote_style: None, + keyword: Keyword::SELECT, + }), + Span::empty() + )), + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![ + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("field1"))), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("field2"))), + ], + exclude: None, + into: None, + from: vec![TableWithJoins { + relation: table_from_name(ObjectName::from(vec![ + Ident::new("mydataset"), + Ident::new("table1") + ])), + joins: vec![], + }], + lateral_views: vec![], + prewhere: None, + selection: None, + group_by: GroupByExpr::Expressions(vec![], vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + window_before_qualify: false, + value_table_mode: None, + connect_by: None, + flavor: SelectFlavor::Standard, + }))), + order_by: Some(OrderBy { + kind: OrderByKind::Expressions(vec![OrderByExpr { + expr: Expr::Identifier(Ident::new("field1")), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + },]), + interpolate: None, + }), + limit_clause: Some(LimitClause::LimitOffset { + limit: Some(Expr::Value(number("10").with_empty_span())), + offset: None, + limit_by: vec![], + }), + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + pipe_operators: vec![], + }) + }) + ); + + let stmt = bigquery().verified_stmt(concat!( + "EXPORT DATA WITH CONNECTION myconnection.myproject.us OPTIONS(", + "uri = 'gs://bucket/folder/*', ", + "format = 'PARQUET', ", + "overwrite = true", + ") AS ", + "SELECT field1, field2 FROM mydataset.table1 ORDER BY field1 LIMIT 10", + )); + + assert_eq!( + stmt, + Statement::ExportData(ExportData { + options: vec![ + SqlOption::KeyValue { + key: Ident::new("uri"), + value: Expr::Value( + Value::SingleQuotedString("gs://bucket/folder/*".to_owned()) + .with_empty_span() + ), + }, + SqlOption::KeyValue { + key: Ident::new("format"), + value: Expr::Value( + Value::SingleQuotedString("PARQUET".to_owned()).with_empty_span() + ), + }, + SqlOption::KeyValue { + key: Ident::new("overwrite"), + value: Expr::Value(Value::Boolean(true).with_empty_span()), + }, + ], + connection: Some(ObjectName::from(vec![ + Ident::new("myconnection"), + Ident::new("myproject"), + Ident::new("us") + ])), + query: Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken(TokenWithSpan::new( + Token::Word(Word { + value: "SELECT".to_string(), + quote_style: None, + keyword: Keyword::SELECT, + }), + Span::empty() + )), + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![ + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("field1"))), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("field2"))), + ], + exclude: None, + into: None, + from: vec![TableWithJoins { + relation: table_from_name(ObjectName::from(vec![ + Ident::new("mydataset"), + Ident::new("table1") + ])), + joins: vec![], + }], + lateral_views: vec![], + prewhere: None, + selection: None, + group_by: GroupByExpr::Expressions(vec![], vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + window_before_qualify: false, + value_table_mode: None, + connect_by: None, + flavor: SelectFlavor::Standard, + }))), + order_by: Some(OrderBy { + kind: OrderByKind::Expressions(vec![OrderByExpr { + expr: Expr::Identifier(Ident::new("field1")), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + },]), + interpolate: None, + }), + limit_clause: Some(LimitClause::LimitOffset { + limit: Some(Expr::Value(number("10").with_empty_span())), + offset: None, + limit_by: vec![], + }), + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + pipe_operators: vec![], + }) + }) + ); + + // at least one option (uri) is required + let err = bigquery() + .parse_sql_statements(concat!( + "EXPORT DATA OPTIONS() AS ", + "SELECT field1, field2 FROM mydataset.table1 ORDER BY field1 LIMIT 10", + )) + .unwrap_err(); + assert_eq!( + err.to_string(), + "sql parser error: Expected: identifier, found: )" + ); + + let err = bigquery() + .parse_sql_statements( + "EXPORT DATA AS SELECT field1, field2 FROM mydataset.table1 ORDER BY field1 LIMIT 10", + ) + .unwrap_err(); + assert_eq!( + err.to_string(), + "sql parser error: Expected: OPTIONS, found: AS" + ); +} + +#[test] +fn test_begin_transaction() { + bigquery().verified_stmt("BEGIN TRANSACTION"); +} + +#[test] +fn test_begin_statement() { + bigquery().verified_stmt("BEGIN"); +} + +#[test] +fn test_alter_schema() { + bigquery_and_generic().verified_stmt("ALTER SCHEMA mydataset SET DEFAULT COLLATE 'und:ci'"); + bigquery_and_generic().verified_stmt("ALTER SCHEMA mydataset ADD REPLICA 'us'"); + bigquery_and_generic() + .verified_stmt("ALTER SCHEMA mydataset ADD REPLICA 'us' OPTIONS (location = 'us')"); + bigquery_and_generic().verified_stmt("ALTER SCHEMA mydataset DROP REPLICA 'us'"); + bigquery_and_generic().verified_stmt("ALTER SCHEMA mydataset SET OPTIONS (location = 'us')"); + bigquery_and_generic() + .verified_stmt("ALTER SCHEMA IF EXISTS mydataset SET OPTIONS (location = 'us')"); +} diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index c56f98860..bc1431f9c 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -28,7 +28,7 @@ use test_utils::*; use sqlparser::ast::Expr::{BinaryOp, Identifier}; use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::Table; -use sqlparser::ast::Value::Number; +use sqlparser::ast::Value::Boolean; use sqlparser::ast::*; use sqlparser::dialect::ClickHouseDialect; use sqlparser::dialect::GenericDialect; @@ -60,6 +60,7 @@ fn parse_map_access_expr() { ), })], })], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("foos")])), @@ -219,10 +220,14 @@ fn parse_delimited_identifiers() { #[test] fn parse_create_table() { - clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#); - clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY "x""#); + clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY ("x")"#); + clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY "x""#); clickhouse().verified_stmt( - r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY "x" AS SELECT * FROM "t" WHERE true"#, + r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY "x" AS SELECT * FROM "t" WHERE true"#, + ); + clickhouse().one_statement_parses_to( + "CREATE TABLE x (a int) ENGINE = MergeTree() ORDER BY a", + "CREATE TABLE x (a INT) ENGINE = MergeTree ORDER BY a", ); } @@ -589,7 +594,7 @@ fn parse_clickhouse_data_types() { #[test] fn parse_create_table_with_nullable() { - let sql = r#"CREATE TABLE table (k UInt8, `a` Nullable(String), `b` Nullable(DateTime64(9, 'UTC')), c Nullable(DateTime64(9)), d Date32 NULL) ENGINE=MergeTree ORDER BY (`k`)"#; + let sql = r#"CREATE TABLE table (k UInt8, `a` Nullable(String), `b` Nullable(DateTime64(9, 'UTC')), c Nullable(DateTime64(9)), d Date32 NULL) ENGINE = MergeTree ORDER BY (`k`)"#; // ClickHouse has a case-sensitive definition of data type, but canonical representation is not let canonical_sql = sql.replace("String", "STRING"); @@ -669,11 +674,13 @@ fn parse_create_table_with_nested_data_types() { DataType::Tuple(vec![ StructField { field_name: None, - field_type: DataType::FixedString(128) + field_type: DataType::FixedString(128), + options: None, }, StructField { field_name: None, - field_type: DataType::Int128 + field_type: DataType::Int128, + options: None, } ]) ))), @@ -685,12 +692,14 @@ fn parse_create_table_with_nested_data_types() { StructField { field_name: Some("a".into()), field_type: DataType::Datetime64(9, None), + options: None, }, StructField { field_name: Some("b".into()), field_type: DataType::Array(ArrayElemTypeDef::Parenthesis( Box::new(DataType::Uuid) - )) + )), + options: None, }, ]), options: vec![], @@ -714,14 +723,14 @@ fn parse_create_table_with_nested_data_types() { fn parse_create_table_with_primary_key() { match clickhouse_and_generic().verified_stmt(concat!( r#"CREATE TABLE db.table (`i` INT, `k` INT)"#, - " ENGINE=SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')", + " ENGINE = SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')", " PRIMARY KEY tuple(i)", " ORDER BY tuple(i)", )) { Statement::CreateTable(CreateTable { name, columns, - engine, + table_options, primary_key, order_by, .. @@ -742,16 +751,23 @@ fn parse_create_table_with_primary_key() { ], columns ); - assert_eq!( - engine, - Some(TableEngine { - name: "SharedMergeTree".to_string(), - parameters: Some(vec![ + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new("SharedMergeTree")), + values: vec![ Ident::with_quote('\'', "/clickhouse/tables/{uuid}/{shard}"), Ident::with_quote('\'', "{replica}"), - ]), - }) - ); + ] + } + ))); + fn assert_function(actual: &Function, name: &str, arg: &str) -> bool { assert_eq!(actual.name, ObjectName::from(vec![Ident::new(name)])); assert_eq!( @@ -798,7 +814,7 @@ fn parse_create_table_with_variant_default_expressions() { " b DATETIME EPHEMERAL now(),", " c DATETIME EPHEMERAL,", " d STRING ALIAS toString(c)", - ") ENGINE=MergeTree" + ") ENGINE = MergeTree" ); match clickhouse_and_generic().verified_stmt(sql) { Statement::CreateTable(CreateTable { columns, .. }) => { @@ -903,7 +919,7 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, }, ViewColumnDef { name: "f".into(), @@ -915,7 +931,7 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, }, ] ); @@ -954,38 +970,103 @@ fn parse_limit_by() { #[test] fn parse_settings_in_query() { - match clickhouse_and_generic() - .verified_stmt(r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#) - { - Statement::Query(query) => { - assert_eq!( - query.settings, - Some(vec![ - Setting { - key: Ident::new("max_threads"), - value: Number("1".parse().unwrap(), false) - }, - Setting { - key: Ident::new("max_block_size"), - value: Number("10000".parse().unwrap(), false) - }, - ]) - ); + fn check_settings(sql: &str, expected: Vec) { + match clickhouse_and_generic().verified_stmt(sql) { + Statement::Query(q) => { + assert_eq!(q.settings, Some(expected)); + } + _ => unreachable!(), } - _ => unreachable!(), + } + + for (sql, expected_settings) in [ + ( + r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#, + vec![ + Setting { + key: Ident::new("max_threads"), + value: Expr::value(number("1")), + }, + Setting { + key: Ident::new("max_block_size"), + value: Expr::value(number("10000")), + }, + ], + ), + ( + r#"SELECT * FROM t SETTINGS additional_table_filters = {'table_1': 'x != 2'}"#, + vec![Setting { + key: Ident::new("additional_table_filters"), + value: Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "table_1"), + value: Expr::value(single_quoted_string("x != 2")).into(), + }]), + }], + ), + ( + r#"SELECT * FROM t SETTINGS additional_result_filter = 'x != 2', query_plan_optimize_lazy_materialization = false"#, + vec![ + Setting { + key: Ident::new("additional_result_filter"), + value: Expr::value(single_quoted_string("x != 2")), + }, + Setting { + key: Ident::new("query_plan_optimize_lazy_materialization"), + value: Expr::value(Boolean(false)), + }, + ], + ), + ] { + check_settings(sql, expected_settings); } let invalid_cases = vec![ - "SELECT * FROM t SETTINGS a", - "SELECT * FROM t SETTINGS a=", - "SELECT * FROM t SETTINGS a=1, b", - "SELECT * FROM t SETTINGS a=1, b=", - "SELECT * FROM t SETTINGS a=1, b=c", + ("SELECT * FROM t SETTINGS a", "Expected: =, found: EOF"), + ( + "SELECT * FROM t SETTINGS a=", + "Expected: an expression, found: EOF", + ), + ("SELECT * FROM t SETTINGS a=1, b", "Expected: =, found: EOF"), + ( + "SELECT * FROM t SETTINGS a=1, b=", + "Expected: an expression, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {", + "Expected: identifier, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {'b'", + "Expected: :, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': ", + "Expected: an expression, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': 'c',}", + "Expected: identifier, found: }", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': 'c', 'd'}", + "Expected: :, found: }", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': 'c', 'd': }", + "Expected: an expression, found: }", + ), + ( + "SELECT * FROM t SETTINGS a = {ANY(b)}", + "Expected: :, found: (", + ), ]; - for sql in invalid_cases { - clickhouse_and_generic() - .parse_sql_statements(sql) - .expect_err("Expected: SETTINGS key = value, found: "); + for (sql, error_msg) in invalid_cases { + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError(error_msg.to_string()) + ); } } #[test] @@ -1334,7 +1415,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - clickhouse().verified_stmt(&format!("USE {}", object_name)), + clickhouse().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -1342,7 +1423,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - clickhouse().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), + clickhouse().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -1356,7 +1437,7 @@ fn parse_use() { fn test_query_with_format_clause() { let format_options = vec!["TabSeparated", "JSONCompact", "NULL"]; for format in &format_options { - let sql = format!("SELECT * FROM t FORMAT {}", format); + let sql = format!("SELECT * FROM t FORMAT {format}"); match clickhouse_and_generic().verified_stmt(&sql) { Statement::Query(query) => { if *format == "NULL" { @@ -1539,11 +1620,11 @@ fn parse_select_table_function_settings() { settings: Some(vec![ Setting { key: "s0".into(), - value: Value::Number("3".parse().unwrap(), false), + value: Expr::value(number("3")), }, Setting { key: "s1".into(), - value: Value::SingleQuotedString("s".into()), + value: Expr::value(single_quoted_string("s")), }, ]), }, @@ -1564,11 +1645,11 @@ fn parse_select_table_function_settings() { settings: Some(vec![ Setting { key: "s0".into(), - value: Value::Number("3".parse().unwrap(), false), + value: Expr::value(number("3")), }, Setting { key: "s1".into(), - value: Value::SingleQuotedString("s".into()), + value: Expr::value(single_quoted_string("s")), }, ]), }, @@ -1578,7 +1659,6 @@ fn parse_select_table_function_settings() { "SELECT * FROM t(SETTINGS a=)", "SELECT * FROM t(SETTINGS a=1, b)", "SELECT * FROM t(SETTINGS a=1, b=)", - "SELECT * FROM t(SETTINGS a=1, b=c)", ]; for sql in invalid_cases { clickhouse_and_generic() @@ -1625,6 +1705,30 @@ fn parse_table_sample() { clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1 / 10 OFFSET 1 / 2"); } +#[test] +fn test_parse_not_null_in_column_options() { + // In addition to DEFAULT and CHECK ClickHouse also supports MATERIALIZED, all of which + // can contain `IS NOT NULL` and thus `NOT NULL` as an alias. + let canonical = concat!( + "CREATE TABLE foo (", + "abc INT DEFAULT (42 IS NOT NULL) NOT NULL,", + " not_null BOOL MATERIALIZED (abc IS NOT NULL),", + " CHECK (abc IS NOT NULL)", + ")", + ); + clickhouse().verified_stmt(canonical); + clickhouse().one_statement_parses_to( + concat!( + "CREATE TABLE foo (", + "abc INT DEFAULT (42 NOT NULL) NOT NULL,", + " not_null BOOL MATERIALIZED (abc NOT NULL),", + " CHECK (abc NOT NULL)", + ")", + ), + canonical, + ); +} + fn clickhouse() -> TestedDialects { TestedDialects::new(vec![Box::new(ClickHouseDialect {})]) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fa2346c2c..8b99bb1dc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -27,6 +27,8 @@ extern crate core; use helpers::attached_token::AttachedToken; use matches::assert_matches; +use sqlparser::ast::helpers::key_value_options::*; +use sqlparser::ast::helpers::key_value_options::{KeyValueOptions, KeyValueOptionsDelimiter}; use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::{Pivot, Unpivot}; use sqlparser::ast::*; @@ -40,8 +42,9 @@ use sqlparser::parser::{Parser, ParserError, ParserOptions}; use sqlparser::tokenizer::Tokenizer; use sqlparser::tokenizer::{Location, Span}; use test_utils::{ - all_dialects, all_dialects_where, alter_table_op, assert_eq_vec, call, expr_from_projection, - join, number, only, table, table_alias, table_from_name, TestedDialects, + all_dialects, all_dialects_where, all_dialects_with_options, alter_table_op, assert_eq_vec, + call, expr_from_projection, join, number, only, table, table_alias, table_from_name, + TestedDialects, }; #[macro_use] @@ -459,6 +462,7 @@ fn parse_update_set_from() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))), ], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), @@ -489,6 +493,7 @@ fn parse_update_set_from() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), alias: Some(TableAlias { name: Ident::new("t2"), @@ -510,6 +515,7 @@ fn parse_update_set_from() { }), returning: None, or: None, + limit: None } ); @@ -528,6 +534,7 @@ fn parse_update_with_table_alias() { selection, returning, or: None, + limit: None, } => { assert_eq!( TableWithJoins { @@ -1231,7 +1238,6 @@ fn parse_select_expr_star() { "SELECT 2. * 3 FROM T", ); dialects.verified_only_select("SELECT myfunc().* FROM T"); - dialects.verified_only_select("SELECT myfunc().* EXCEPT (foo) FROM T"); // Invalid let res = dialects.parse_sql_statements("SELECT foo.*.* FROM T"); @@ -1239,6 +1245,11 @@ fn parse_select_expr_star() { ParserError::ParserError("Expected: end of statement, found: .".to_string()), res.unwrap_err() ); + + let dialects = all_dialects_where(|d| { + d.supports_select_expr_star() && d.supports_select_wildcard_except() + }); + dialects.verified_only_select("SELECT myfunc().* EXCEPT (foo) FROM T"); } #[test] @@ -2039,7 +2050,7 @@ fn parse_ilike() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), any: false, }, select.selection.unwrap() @@ -2103,7 +2114,7 @@ fn parse_like() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), any: false, }, select.selection.unwrap() @@ -2166,7 +2177,24 @@ fn parse_similar_to() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), + }, + select.selection.unwrap() + ); + + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE NULL", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), + escape_char: Some(Value::Null), }, select.selection.unwrap() ); @@ -2184,7 +2212,7 @@ fn parse_similar_to() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), })), select.selection.unwrap() ); @@ -2224,7 +2252,7 @@ fn parse_in_subquery() { assert_eq!( Expr::InSubquery { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - subquery: verified_query("SELECT segm FROM bar").body, + subquery: Box::new(verified_query("SELECT segm FROM bar")), negated: false, }, select.selection.unwrap() @@ -2238,7 +2266,9 @@ fn parse_in_union() { assert_eq!( Expr::InSubquery { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - subquery: verified_query("(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)").body, + subquery: Box::new(verified_query( + "(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)" + )), negated: false, }, select.selection.unwrap() @@ -2821,6 +2851,38 @@ fn parse_group_by_special_grouping_sets() { } } +#[test] +fn parse_group_by_grouping_sets_single_values() { + let sql = "SELECT a, b, SUM(c) FROM tab1 GROUP BY a, b GROUPING SETS ((a, b), a, (b), c, ())"; + let canonical = + "SELECT a, b, SUM(c) FROM tab1 GROUP BY a, b GROUPING SETS ((a, b), (a), (b), (c), ())"; + match all_dialects().one_statement_parses_to(sql, canonical) { + Statement::Query(query) => { + let group_by = &query.body.as_select().unwrap().group_by; + assert_eq!( + group_by, + &GroupByExpr::Expressions( + vec![ + Expr::Identifier(Ident::new("a")), + Expr::Identifier(Ident::new("b")) + ], + vec![GroupByWithModifier::GroupingSets(Expr::GroupingSets(vec![ + vec![ + Expr::Identifier(Ident::new("a")), + Expr::Identifier(Ident::new("b")) + ], + vec![Expr::Identifier(Ident::new("a"))], + vec![Expr::Identifier(Ident::new("b"))], + vec![Expr::Identifier(Ident::new("c"))], + vec![] + ]))] + ) + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_select_having() { let sql = "SELECT foo FROM bar GROUP BY foo HAVING COUNT(*) > 1"; @@ -3528,7 +3590,7 @@ fn test_double_value() { for (input, expected) in test_cases { for (i, expr) in input.iter().enumerate() { if let Statement::Query(query) = - dialects.one_statement_parses_to(&format!("SELECT {}", expr), "") + dialects.one_statement_parses_to(&format!("SELECT {expr}"), "") { if let SetExpr::Select(select) = *query.body { assert_eq!(expected[i], select.projection[0]); @@ -3656,7 +3718,7 @@ fn parse_create_table() { name, columns, constraints, - with_options, + table_options, if_not_exists: false, external: false, file_format: None, @@ -3758,6 +3820,7 @@ fn parse_create_table() { vec![ TableConstraint::ForeignKey { name: Some("fkey".into()), + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], @@ -3767,6 +3830,7 @@ fn parse_create_table() { }, TableConstraint::ForeignKey { name: Some("fkey2".into()), + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], @@ -3776,6 +3840,7 @@ fn parse_create_table() { }, TableConstraint::ForeignKey { name: None, + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], @@ -3785,6 +3850,7 @@ fn parse_create_table() { }, TableConstraint::ForeignKey { name: None, + index_name: None, columns: vec!["lng".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], @@ -3794,7 +3860,7 @@ fn parse_create_table() { }, ] ); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); } _ => unreachable!(), } @@ -3839,7 +3905,7 @@ fn parse_create_table_with_constraint_characteristics() { name, columns, constraints, - with_options, + table_options, if_not_exists: false, external: false, file_format: None, @@ -3881,6 +3947,7 @@ fn parse_create_table_with_constraint_characteristics() { vec![ TableConstraint::ForeignKey { name: Some("fkey".into()), + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], @@ -3894,6 +3961,7 @@ fn parse_create_table_with_constraint_characteristics() { }, TableConstraint::ForeignKey { name: Some("fkey2".into()), + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], @@ -3907,6 +3975,7 @@ fn parse_create_table_with_constraint_characteristics() { }, TableConstraint::ForeignKey { name: None, + index_name: None, columns: vec!["lat".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], @@ -3920,6 +3989,7 @@ fn parse_create_table_with_constraint_characteristics() { }, TableConstraint::ForeignKey { name: None, + index_name: None, columns: vec!["lng".into()], foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], @@ -3933,7 +4003,7 @@ fn parse_create_table_with_constraint_characteristics() { }, ] ); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); } _ => unreachable!(), } @@ -3980,13 +4050,13 @@ fn parse_create_table_column_constraint_characteristics() { syntax }; - let sql = format!("CREATE TABLE t (a int UNIQUE {})", syntax); + let sql = format!("CREATE TABLE t (a int UNIQUE {syntax})"); let expected_clause = if syntax.is_empty() { String::new() } else { format!(" {syntax}") }; - let expected = format!("CREATE TABLE t (a INT UNIQUE{})", expected_clause); + let expected = format!("CREATE TABLE t (a INT UNIQUE{expected_clause})"); let ast = one_statement_parses_to(&sql, &expected); let expected_value = if deferrable.is_some() || initially.is_some() || enforced.is_some() { @@ -4227,6 +4297,10 @@ fn parse_create_schema() { verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a OPTIONS(key1 = 'value1')"#); verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a OPTIONS()"#); verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a DEFAULT COLLATE 'und:ci' OPTIONS()"#); + verified_stmt(r#"CREATE SCHEMA a.b.c WITH (key1 = 'value1', key2 = 'value2')"#); + verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a WITH (key1 = 'value1')"#); + verified_stmt(r#"CREATE SCHEMA IF NOT EXISTS a WITH ()"#); + verified_stmt(r#"CREATE SCHEMA a CLONE b"#); } #[test] @@ -4278,8 +4352,9 @@ fn parse_create_table_as() { // BigQuery allows specifying table schema in CTAS // ANSI SQL and PostgreSQL let you only specify the list of columns // (without data types) in a CTAS, but we have yet to support that. + let dialects = all_dialects_where(|d| d.supports_create_table_multi_schema_info_sources()); let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a"; - match verified_stmt(sql) { + match dialects.verified_stmt(sql) { Statement::CreateTable(CreateTable { columns, query, .. }) => { assert_eq!(columns.len(), 2); assert_eq!(columns[0].to_string(), "a INT".to_string()); @@ -4310,6 +4385,7 @@ fn parse_create_table_as_table() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }); match verified_stmt(sql1) { @@ -4335,6 +4411,7 @@ fn parse_create_table_as_table() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }); match verified_stmt(sql2) { @@ -4382,20 +4459,6 @@ fn parse_create_or_replace_table() { } _ => unreachable!(), } - - let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a"; - match verified_stmt(sql) { - Statement::CreateTable(CreateTable { columns, query, .. }) => { - assert_eq!(columns.len(), 2); - assert_eq!(columns[0].to_string(), "a INT".to_string()); - assert_eq!(columns[1].to_string(), "b INT".to_string()); - assert_eq!( - query, - Some(Box::new(verified_query("SELECT 1 AS b, 2 AS a"))) - ); - } - _ => unreachable!(), - } } #[test] @@ -4418,7 +4481,11 @@ fn parse_create_table_with_options() { let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)"; match generic.verified_stmt(sql) { - Statement::CreateTable(CreateTable { with_options, .. }) => { + Statement::CreateTable(CreateTable { table_options, .. }) => { + let with_options = match table_options { + CreateTableOptions::With(options) => options, + _ => unreachable!(), + }; assert_eq!( vec![ SqlOption::KeyValue { @@ -4479,7 +4546,7 @@ fn parse_create_external_table() { name, columns, constraints, - with_options, + table_options, if_not_exists, external, file_format, @@ -4522,7 +4589,7 @@ fn parse_create_external_table() { assert_eq!(FileFormat::TEXTFILE, file_format.unwrap()); assert_eq!("/tmp/example.csv", location.unwrap()); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); assert!(!if_not_exists); } _ => unreachable!(), @@ -4547,7 +4614,7 @@ fn parse_create_or_replace_external_table() { name, columns, constraints, - with_options, + table_options, if_not_exists, external, file_format, @@ -4576,7 +4643,7 @@ fn parse_create_or_replace_external_table() { assert_eq!(FileFormat::TEXTFILE, file_format.unwrap()); assert_eq!("/tmp/example.csv", location.unwrap()); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); assert!(!if_not_exists); assert!(or_replace); } @@ -4627,7 +4694,21 @@ fn parse_alter_table() { let rename_table = "ALTER TABLE tab RENAME TO new_tab"; match alter_table_op(verified_stmt(rename_table)) { AlterTableOperation::RenameTable { table_name } => { - assert_eq!("new_tab", table_name.to_string()); + assert_eq!( + RenameTableNameKind::To(ObjectName::from(vec![Ident::new("new_tab")])), + table_name + ); + } + _ => unreachable!(), + }; + + let rename_table_as = "ALTER TABLE tab RENAME AS new_tab"; + match alter_table_op(verified_stmt(rename_table_as)) { + AlterTableOperation::RenameTable { table_name } => { + assert_eq!( + RenameTableNameKind::As(ObjectName::from(vec![Ident::new("new_tab")])), + table_name + ); } _ => unreachable!(), }; @@ -4663,6 +4744,34 @@ fn parse_alter_table() { } _ => unreachable!(), } + + let set_storage_parameters = "ALTER TABLE tab SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_vacuum_threshold = 500)"; + match alter_table_op(verified_stmt(set_storage_parameters)) { + AlterTableOperation::SetOptionsParens { options } => { + assert_eq!( + options, + [ + SqlOption::KeyValue { + key: Ident { + value: "autovacuum_vacuum_scale_factor".to_string(), + quote_style: None, + span: Span::empty(), + }, + value: Expr::Value(test_utils::number("0.01").with_empty_span()), + }, + SqlOption::KeyValue { + key: Ident { + value: "autovacuum_vacuum_threshold".to_string(), + quote_style: None, + span: Span::empty(), + }, + value: Expr::Value(test_utils::number("500").with_empty_span()), + } + ], + ); + } + _ => unreachable!(), + } } #[test] @@ -4904,7 +5013,7 @@ fn parse_alter_table_constraints() { match alter_table_op(verified_stmt(&format!( "ALTER TABLE tab ADD {constraint_text}" ))) { - AlterTableOperation::AddConstraint(constraint) => { + AlterTableOperation::AddConstraint { constraint, .. } => { assert_eq!(constraint_text, constraint.to_string()); } _ => unreachable!(), @@ -4919,22 +5028,26 @@ fn parse_alter_table_drop_column() { check_one("DROP COLUMN IF EXISTS is_active CASCADE"); check_one("DROP COLUMN IF EXISTS is_active RESTRICT"); one_statement_parses_to( - "ALTER TABLE tab DROP IF EXISTS is_active CASCADE", + "ALTER TABLE tab DROP COLUMN IF EXISTS is_active CASCADE", "ALTER TABLE tab DROP COLUMN IF EXISTS is_active CASCADE", ); one_statement_parses_to( "ALTER TABLE tab DROP is_active CASCADE", - "ALTER TABLE tab DROP COLUMN is_active CASCADE", + "ALTER TABLE tab DROP is_active CASCADE", ); + let dialects = all_dialects_where(|d| d.supports_comma_separated_drop_column_list()); + dialects.verified_stmt("ALTER TABLE tbl DROP COLUMN c1, c2, c3"); + fn check_one(constraint_text: &str) { match alter_table_op(verified_stmt(&format!("ALTER TABLE tab {constraint_text}"))) { AlterTableOperation::DropColumn { - column_name, + has_column_keyword: true, + column_names, if_exists, drop_behavior, } => { - assert_eq!("is_active", column_name.to_string()); + assert_eq!("is_active", column_names.first().unwrap().to_string()); assert!(if_exists); match drop_behavior { None => assert!(constraint_text.ends_with(" is_active")), @@ -5004,22 +5117,21 @@ fn parse_alter_table_alter_column_type() { AlterColumnOperation::SetDataType { data_type: DataType::Text, using: None, + had_set: true, } ); } _ => unreachable!(), } + verified_stmt(&format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT")); - let dialect = TestedDialects::new(vec![Box::new(GenericDialect {})]); - - let res = - dialect.parse_sql_statements(&format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT")); - assert_eq!( - ParserError::ParserError("Expected: SET/DROP NOT NULL, SET DEFAULT, or SET DATA TYPE after ALTER COLUMN, found: TYPE".to_string()), - res.unwrap_err() - ); + let dialects = all_dialects_where(|d| d.supports_alter_column_type_using()); + dialects.verified_stmt(&format!( + "{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'" + )); - let res = dialect.parse_sql_statements(&format!( + let dialects = all_dialects_except(|d| d.supports_alter_column_type_using()); + let res = dialects.parse_sql_statements(&format!( "{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'" )); assert_eq!( @@ -5096,7 +5208,7 @@ fn run_explain_analyze( query: &str, expected_verbose: bool, expected_analyze: bool, - expected_format: Option, + expected_format: Option, expected_options: Option>, ) { match dialect.verified_stmt(query) { @@ -5207,7 +5319,7 @@ fn parse_explain_analyze_with_simple_select() { "EXPLAIN ANALYZE FORMAT GRAPHVIZ SELECT sqrt(id) FROM foo", false, true, - Some(AnalyzeFormat::GRAPHVIZ), + Some(AnalyzeFormatKind::Keyword(AnalyzeFormat::GRAPHVIZ)), None, ); @@ -5216,7 +5328,16 @@ fn parse_explain_analyze_with_simple_select() { "EXPLAIN ANALYZE VERBOSE FORMAT JSON SELECT sqrt(id) FROM foo", true, true, - Some(AnalyzeFormat::JSON), + Some(AnalyzeFormatKind::Keyword(AnalyzeFormat::JSON)), + None, + ); + + run_explain_analyze( + all_dialects(), + "EXPLAIN ANALYZE VERBOSE FORMAT=JSON SELECT sqrt(id) FROM foo", + true, + true, + Some(AnalyzeFormatKind::Assignment(AnalyzeFormat::JSON)), None, ); @@ -5225,7 +5346,7 @@ fn parse_explain_analyze_with_simple_select() { "EXPLAIN VERBOSE FORMAT TEXT SELECT sqrt(id) FROM foo", true, false, - Some(AnalyzeFormat::TEXT), + Some(AnalyzeFormatKind::Keyword(AnalyzeFormat::TEXT)), None, ); } @@ -5472,7 +5593,8 @@ fn parse_named_window_functions() { WINDOW w AS (PARTITION BY x), win AS (ORDER BY y)"; supported_dialects.verified_stmt(sql); - let select = verified_only_select(sql); + let select = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))) + .verified_only_select(sql); const EXPECTED_PROJ_QTY: usize = 2; assert_eq!(EXPECTED_PROJ_QTY, select.projection.len()); @@ -5502,6 +5624,7 @@ fn parse_named_window_functions() { #[test] fn parse_window_clause() { + let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))); let sql = "SELECT * \ FROM mytable \ WINDOW \ @@ -5514,10 +5637,14 @@ fn parse_window_clause() { window7 AS (window1 ROWS UNBOUNDED PRECEDING), \ window8 AS (window1 PARTITION BY a ORDER BY b ROWS UNBOUNDED PRECEDING) \ ORDER BY C3"; - verified_only_select(sql); + dialects.verified_only_select(sql); let sql = "SELECT * from mytable WINDOW window1 AS window2"; - let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let dialects = all_dialects_except(|d| { + d.is::() + || d.is::() + || d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d)) + }); let res = dialects.parse_sql_statements(sql); assert_eq!( ParserError::ParserError("Expected: (, found: window2".to_string()), @@ -5527,6 +5654,7 @@ fn parse_window_clause() { #[test] fn test_parse_named_window() { + let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))); let sql = "SELECT \ MIN(c12) OVER window1 AS min1, \ MAX(c12) OVER window2 AS max1 \ @@ -5534,7 +5662,7 @@ fn test_parse_named_window() { WINDOW window1 AS (ORDER BY C12), \ window2 AS (PARTITION BY C11) \ ORDER BY C3"; - let actual_select_only = verified_only_select(sql); + let actual_select_only = dialects.verified_only_select(sql); let expected = Select { select_token: AttachedToken::empty(), distinct: None, @@ -5612,6 +5740,7 @@ fn test_parse_named_window() { }, }, ], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident { @@ -5683,6 +5812,10 @@ fn test_parse_named_window() { #[test] fn parse_window_and_qualify_clause() { + let dialects = all_dialects_except(|d| { + d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d)) + || d.is_table_alias(&Keyword::QUALIFY, &mut Parser::new(d)) + }); let sql = "SELECT \ MIN(c12) OVER window1 AS min1 \ FROM aggregate_test_100 \ @@ -5690,7 +5823,7 @@ fn parse_window_and_qualify_clause() { WINDOW window1 AS (ORDER BY C12), \ window2 AS (PARTITION BY C11) \ ORDER BY C3"; - verified_only_select(sql); + dialects.verified_only_select(sql); let sql = "SELECT \ MIN(c12) OVER window1 AS min1 \ @@ -5699,7 +5832,7 @@ fn parse_window_and_qualify_clause() { window2 AS (PARTITION BY C11) \ QUALIFY ROW_NUMBER() OVER my_window \ ORDER BY C3"; - verified_only_select(sql); + dialects.verified_only_select(sql); } #[test] @@ -5797,10 +5930,14 @@ fn parse_literal_date() { let sql = "SELECT DATE '1999-01-01'"; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::Date, - value: Value::SingleQuotedString("1999-01-01".into()), - }, + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); } @@ -5810,10 +5947,14 @@ fn parse_literal_time() { let sql = "SELECT TIME '01:23:34'"; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: Value::SingleQuotedString("01:23:34".into()), - }, + value: ValueWithSpan { + value: Value::SingleQuotedString("01:23:34".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); } @@ -5823,10 +5964,14 @@ fn parse_literal_datetime() { let sql = "SELECT DATETIME '1999-01-01 01:23:34.45'"; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::Datetime(None), - value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), - }, + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); } @@ -5836,10 +5981,14 @@ fn parse_literal_timestamp_without_time_zone() { let sql = "SELECT TIMESTAMP '1999-01-01 01:23:34'"; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("1999-01-01 01:23:34".into()), - }, + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); @@ -5851,10 +6000,14 @@ fn parse_literal_timestamp_with_time_zone() { let sql = "SELECT TIMESTAMPTZ '1999-01-01 01:23:34Z'"; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::Tz), - value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()), - }, + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); @@ -6249,6 +6402,7 @@ fn parse_interval_and_or_xor() { quote_style: None, span: Span::empty(), }))], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident { @@ -6332,6 +6486,7 @@ fn parse_interval_and_or_xor() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }))]; assert_eq!(actual_ast, expected_ast); @@ -6422,10 +6577,11 @@ fn parse_json_keyword() { }'"#; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString( - r#"{ + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{ "id": 10, "type": "fruit", "name": "apple", @@ -6445,9 +6601,12 @@ fn parse_json_keyword() { ] } }"# - .to_string() - ) - }, + .to_string() + ), + span: Span::empty(), + }, + uses_odbc_syntax: false, + }), expr_from_projection(only(&select.projection)), ); } @@ -6456,14 +6615,23 @@ fn parse_json_keyword() { fn parse_typed_strings() { let expr = verified_expr(r#"JSON '{"foo":"bar"}'"#); assert_eq!( - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()) - }, + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr ); - if let Expr::TypedString { data_type, value } = expr { + if let Expr::TypedString(TypedString { + data_type, + value, + uses_odbc_syntax: false, + }) = expr + { assert_eq!(DataType::JSON, data_type); assert_eq!(r#"{"foo":"bar"}"#, value.into_string().unwrap()); } @@ -6474,10 +6642,14 @@ fn parse_bignumeric_keyword() { let sql = r#"SELECT BIGNUMERIC '0'"#; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"0"#.into()) - }, + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"0"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); verified_stmt("SELECT BIGNUMERIC '0'"); @@ -6485,10 +6657,14 @@ fn parse_bignumeric_keyword() { let sql = r#"SELECT BIGNUMERIC '123456'"#; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"123456"#.into()) - }, + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"123456"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); verified_stmt("SELECT BIGNUMERIC '123456'"); @@ -6496,10 +6672,14 @@ fn parse_bignumeric_keyword() { let sql = r#"SELECT BIGNUMERIC '-3.14'"#; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"-3.14"#.into()) - }, + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"-3.14"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); verified_stmt("SELECT BIGNUMERIC '-3.14'"); @@ -6507,10 +6687,14 @@ fn parse_bignumeric_keyword() { let sql = r#"SELECT BIGNUMERIC '-0.54321'"#; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"-0.54321"#.into()) - }, + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"-0.54321"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); verified_stmt("SELECT BIGNUMERIC '-0.54321'"); @@ -6518,10 +6702,14 @@ fn parse_bignumeric_keyword() { let sql = r#"SELECT BIGNUMERIC '1.23456e05'"#; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"1.23456e05"#.into()) - }, + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"1.23456e05"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); verified_stmt("SELECT BIGNUMERIC '1.23456e05'"); @@ -6529,10 +6717,14 @@ fn parse_bignumeric_keyword() { let sql = r#"SELECT BIGNUMERIC '-9.876e-3'"#; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"-9.876e-3"#.into()) - }, + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"-9.876e-3"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), expr_from_projection(only(&select.projection)), ); verified_stmt("SELECT BIGNUMERIC '-9.876e-3'"); @@ -6820,6 +7012,8 @@ fn parse_searched_case_expr() { let select = verified_only_select(sql); assert_eq!( &Case { + case_token: AttachedToken::empty(), + end_token: AttachedToken::empty(), operand: None, conditions: vec![ CaseWhen { @@ -6859,6 +7053,8 @@ fn parse_simple_case_expr() { use self::Expr::{Case, Identifier}; assert_eq!( &Case { + case_token: AttachedToken::empty(), + end_token: AttachedToken::empty(), operand: Some(Box::new(Identifier(Ident::new("foo")))), conditions: vec![CaseWhen { condition: Expr::value(number("1")), @@ -7323,7 +7519,8 @@ fn parse_join_syntax_variants() { "SELECT c1 FROM t1 FULL JOIN t2 USING(c1)", ); - let res = parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1"); + let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::OUTER, &mut Parser::new(d))); + let res = dialects.parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1"); assert_eq!( ParserError::ParserError("Expected: APPLY, found: JOIN".to_string()), res.unwrap_err() @@ -7441,7 +7638,7 @@ fn parse_cte_in_data_modification_statements() { assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 1)"); assert!(matches!(*query.body, SetExpr::Update(_))); } - other => panic!("Expected: UPDATE, got: {:?}", other), + other => panic!("Expected: UPDATE, got: {other:?}"), } match verified_stmt("WITH t (x) AS (SELECT 9) DELETE FROM q WHERE id IN (SELECT x FROM t)") { @@ -7449,7 +7646,7 @@ fn parse_cte_in_data_modification_statements() { assert_eq!(query.with.unwrap().to_string(), "WITH t (x) AS (SELECT 9)"); assert!(matches!(*query.body, SetExpr::Delete(_))); } - other => panic!("Expected: DELETE, got: {:?}", other), + other => panic!("Expected: DELETE, got: {other:?}"), } match verified_stmt("WITH x AS (SELECT 42) INSERT INTO t SELECT foo FROM x") { @@ -7457,7 +7654,7 @@ fn parse_cte_in_data_modification_statements() { assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 42)"); assert!(matches!(*query.body, SetExpr::Insert(_))); } - other => panic!("Expected: INSERT, got: {:?}", other), + other => panic!("Expected: INSERT, got: {other:?}"), } } @@ -7704,7 +7901,6 @@ fn parse_trim() { Box::new(MySqlDialect {}), //Box::new(BigQueryDialect {}), Box::new(SQLiteDialect {}), - Box::new(DuckDbDialect {}), ]); assert_eq!( @@ -7769,11 +7965,35 @@ fn parse_create_database() { if_not_exists, location, managed_location, + clone, + .. + } => { + assert_eq!("mydb", db_name.to_string()); + assert!(!if_not_exists); + assert_eq!(None, location); + assert_eq!(None, managed_location); + assert_eq!(None, clone); + } + _ => unreachable!(), + } + let sql = "CREATE DATABASE mydb CLONE otherdb"; + match verified_stmt(sql) { + Statement::CreateDatabase { + db_name, + if_not_exists, + location, + managed_location, + clone, + .. } => { assert_eq!("mydb", db_name.to_string()); assert!(!if_not_exists); assert_eq!(None, location); assert_eq!(None, managed_location); + assert_eq!( + Some(ObjectName::from(vec![Ident::new("otherdb".to_string())])), + clone + ); } _ => unreachable!(), } @@ -7788,11 +8008,14 @@ fn parse_create_database_ine() { if_not_exists, location, managed_location, + clone, + .. } => { assert_eq!("mydb", db_name.to_string()); assert!(if_not_exists); assert_eq!(None, location); assert_eq!(None, managed_location); + assert_eq!(None, clone); } _ => unreachable!(), } @@ -7854,6 +8077,7 @@ fn parse_create_view() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -7922,6 +8146,7 @@ fn parse_create_view_with_columns() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); @@ -7932,7 +8157,7 @@ fn parse_create_view_with_columns() { .map(|name| ViewColumnDef { name, data_type: None, - options: None + options: None, }) .collect::>() ); @@ -7971,6 +8196,7 @@ fn parse_create_view_temporary() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -8010,6 +8236,7 @@ fn parse_create_or_replace_view() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); @@ -8053,6 +8280,7 @@ fn parse_create_or_replace_materialized_view() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); @@ -8092,6 +8320,7 @@ fn parse_create_materialized_view() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -8131,6 +8360,7 @@ fn parse_create_materialized_view_with_cluster_by() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -8232,6 +8462,24 @@ fn parse_drop_view() { verified_stmt("DROP MATERIALIZED VIEW IF EXISTS a.b.c"); } +#[test] +fn parse_drop_user() { + let sql = "DROP USER u1"; + match verified_stmt(sql) { + Statement::Drop { + names, object_type, .. + } => { + assert_eq!( + vec!["u1"], + names.iter().map(ToString::to_string).collect::>() + ); + assert_eq!(ObjectType::User, object_type); + } + _ => unreachable!(), + } + verified_stmt("DROP USER IF EXISTS u1"); +} + #[test] fn parse_invalid_subquery_without_parens() { let res = parse_sql_statements("SELECT SELECT 1 FROM bar WHERE 1=1 FROM baz"); @@ -8489,6 +8737,7 @@ fn lateral_function() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { @@ -8536,8 +8785,11 @@ fn lateral_function() { #[test] fn parse_start_transaction() { let dialects = all_dialects_except(|d| - // BigQuery does not support this syntax - d.is::()); + // BigQuery and Snowflake does not support this syntax + // + // BigQuery: + // Snowflake: + d.is::() || d.is::()); match dialects .verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { @@ -8939,7 +9191,7 @@ fn ensure_multiple_dialects_are_tested() { #[test] fn parse_create_index() { - let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test(name,age DESC)"; + let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test(name, age DESC)"; let indexed_columns: Vec = vec![ IndexColumn { operator_class: None, @@ -8985,7 +9237,7 @@ fn parse_create_index() { #[test] fn test_create_index_with_using_function() { - let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test USING BTREE (name,age DESC)"; + let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test USING BTREE (name, age DESC)"; let indexed_columns: Vec = vec![ IndexColumn { operator_class: None, @@ -9023,6 +9275,8 @@ fn test_create_index_with_using_function() { nulls_distinct: None, with, predicate: None, + index_options, + alter_options, }) => { assert_eq!("idx_name", name.to_string()); assert_eq!("test", table_name.to_string()); @@ -9033,6 +9287,8 @@ fn test_create_index_with_using_function() { assert!(if_not_exists); assert!(include.is_empty()); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -9074,6 +9330,8 @@ fn test_create_index_with_with_clause() { nulls_distinct: None, with, predicate: None, + index_options, + alter_options, }) => { pretty_assertions::assert_eq!("title_idx", name.to_string()); pretty_assertions::assert_eq!("films", table_name.to_string()); @@ -9083,6 +9341,8 @@ fn test_create_index_with_with_clause() { assert!(!if_not_exists); assert!(include.is_empty()); pretty_assertions::assert_eq!(with_parameters, with); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -9164,7 +9424,7 @@ fn parse_drop_role() { #[test] fn parse_grant() { - let sql = "GRANT SELECT, INSERT, UPDATE (shape, size), USAGE, DELETE, TRUNCATE, REFERENCES, TRIGGER, CONNECT, CREATE, EXECUTE, TEMPORARY ON abc, def TO xyz, m WITH GRANT OPTION GRANTED BY jj"; + let sql = "GRANT SELECT, INSERT, UPDATE (shape, size), USAGE, DELETE, TRUNCATE, REFERENCES, TRIGGER, CONNECT, CREATE, EXECUTE, TEMPORARY, DROP ON abc, def TO xyz, m WITH GRANT OPTION GRANTED BY jj"; match verified_stmt(sql) { Statement::Grant { privileges, @@ -9202,6 +9462,7 @@ fn parse_grant() { Action::Create { obj_type: None }, Action::Execute { obj_type: None }, Action::Temporary, + Action::Drop, ], actions ); @@ -9316,13 +9577,62 @@ fn parse_grant() { verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO APPLICATION role1"); verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO APPLICATION ROLE role1"); verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO SHARE share1"); + verified_stmt("GRANT SELECT ON ALL VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON ALL MATERIALIZED VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON ALL EXTERNAL TABLES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT USAGE ON ALL FUNCTIONS IN SCHEMA db1.sc1 TO ROLE role1"); verified_stmt("GRANT USAGE ON SCHEMA sc1 TO a:b"); verified_stmt("GRANT USAGE ON SCHEMA sc1 TO GROUP group1"); verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST"); + verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST COPY CURRENT GRANTS"); + verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST REVOKE CURRENT GRANTS"); verified_stmt("GRANT USAGE ON DATABASE db1 TO ROLE role1"); verified_stmt("GRANT USAGE ON WAREHOUSE wh1 TO ROLE role1"); verified_stmt("GRANT OWNERSHIP ON INTEGRATION int1 TO ROLE role1"); verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1"); + verified_stmt("GRANT EXEC ON my_sp TO runner"); + verified_stmt("GRANT UPDATE ON my_table TO updater_role AS dbo"); + all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) + .verified_stmt("GRANT SELECT ON [my_table] TO [public]"); + verified_stmt("GRANT SELECT ON FUTURE SCHEMAS IN DATABASE db1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE TABLES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE EXTERNAL TABLES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE MATERIALIZED VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE SEQUENCES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT USAGE ON PROCEDURE db1.sc1.foo(INT) TO ROLE role1"); + verified_stmt("GRANT USAGE ON FUNCTION db1.sc1.foo(INT) TO ROLE role1"); + verified_stmt("GRANT ROLE role1 TO ROLE role2"); + verified_stmt("GRANT ROLE role1 TO USER user"); + verified_stmt("GRANT CREATE SCHEMA ON DATABASE db1 TO ROLE role1"); +} + +#[test] +fn parse_deny() { + let sql = "DENY INSERT, DELETE ON users TO analyst CASCADE AS admin"; + match verified_stmt(sql) { + Statement::Deny(deny) => { + assert_eq!( + Privileges::Actions(vec![Action::Insert { columns: None }, Action::Delete]), + deny.privileges + ); + assert_eq!( + &GrantObjects::Tables(vec![ObjectName::from(vec![Ident::new("users")])]), + &deny.objects + ); + assert_eq_vec(&["analyst"], &deny.grantees); + assert_eq!(Some(CascadeOption::Cascade), deny.cascade); + assert_eq!(Some(Ident::from("admin")), deny.granted_by); + } + _ => unreachable!(), + } + + verified_stmt("DENY SELECT, INSERT, UPDATE, DELETE ON db1.sc1 TO role1, role2"); + verified_stmt("DENY ALL ON db1.sc1 TO role1"); + verified_stmt("DENY EXEC ON my_sp TO runner"); + + all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) + .verified_stmt("DENY SELECT ON [my_table] TO [public]"); } #[test] @@ -9437,6 +9747,7 @@ fn parse_merge() { projection: vec![SelectItem::Wildcard( WildcardAdditionalOptions::default() )], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![ @@ -9467,6 +9778,7 @@ fn parse_merge() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), alias: Some(TableAlias { name: Ident { @@ -9590,6 +9902,29 @@ fn parse_merge() { verified_stmt(sql); } +#[test] +fn test_merge_in_cte() { + verified_only_select( + "WITH x AS (\ + MERGE INTO t USING (VALUES (1)) ON 1 = 1 \ + WHEN MATCHED THEN DELETE \ + RETURNING *\ + ) SELECT * FROM x", + ); +} + +#[test] +fn test_merge_with_returning() { + let sql = "MERGE INTO wines AS w \ + USING wine_stock_changes AS s \ + ON s.winename = w.winename \ + WHEN NOT MATCHED AND s.stock_delta > 0 THEN INSERT VALUES (s.winename, s.stock_delta) \ + WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN UPDATE SET stock = w.stock + s.stock_delta \ + WHEN MATCHED THEN DELETE \ + RETURNING merge_action(), w.*"; + verified_stmt(sql); +} + #[test] fn test_merge_with_output() { let sql = "MERGE INTO target_table USING source_table \ @@ -9603,6 +9938,14 @@ fn test_merge_with_output() { verified_stmt(sql); } +#[test] +fn test_merge_with_output_without_into() { + let sql = "MERGE INTO a USING b ON a.id = b.id \ + WHEN MATCHED THEN DELETE \ + OUTPUT inserted.*"; + verified_stmt(sql); +} + #[test] fn test_merge_into_using_table() { let sql = "MERGE INTO target_table USING source_table \ @@ -9948,7 +10291,7 @@ fn parse_offset_and_limit() { #[test] fn parse_time_functions() { fn test_time_function(func_name: &'static str) { - let sql = format!("SELECT {}()", func_name); + let sql = format!("SELECT {func_name}()"); let select = verified_only_select(&sql); let select_localtime_func_call_ast = Function { name: ObjectName::from(vec![Ident::new(func_name)]), @@ -9970,7 +10313,7 @@ fn parse_time_functions() { ); // Validating Parenthesis - let sql_without_parens = format!("SELECT {}", func_name); + let sql_without_parens = format!("SELECT {func_name}"); let mut ast_without_parens = select_localtime_func_call_ast; ast_without_parens.args = FunctionArguments::None; assert_eq!( @@ -10649,7 +10992,10 @@ fn parse_pivot_table() { expected_function("b", Some("t")), expected_function("c", Some("u")), ], - value_column: vec![Ident::new("a"), Ident::new("MONTH")], + value_column: vec![Expr::CompoundIdentifier(vec![ + Ident::new("a"), + Ident::new("MONTH") + ])], value_source: PivotValueSource::List(vec![ ExprWithAlias { expr: Expr::value(number("1")), @@ -10696,24 +11042,20 @@ fn parse_pivot_table() { verified_stmt(sql_without_table_alias).to_string(), sql_without_table_alias ); -} -#[test] -fn parse_unpivot_table() { - let sql = concat!( - "SELECT * FROM sales AS s ", - "UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" + let multiple_value_columns_sql = concat!( + "SELECT * FROM person ", + "PIVOT(", + "SUM(age) AS a, AVG(class) AS c ", + "FOR (name, age) IN (('John', 30) AS c1, ('Mike', 40) AS c2))", ); - pretty_assertions::assert_eq!( - verified_only_select(sql).from[0].relation, - Unpivot { + assert_eq!( + verified_only_select(multiple_value_columns_sql).from[0].relation, + Pivot { table: Box::new(TableFactor::Table { - name: ObjectName::from(vec![Ident::new("sales")]), - alias: Some(TableAlias { - name: Ident::new("s"), - columns: vec![] - }), + name: ObjectName::from(vec![Ident::new("person")]), + alias: None, args: None, with_hints: vec![], version: None, @@ -10723,30 +11065,95 @@ fn parse_unpivot_table() { sample: None, index_hints: vec![], }), - value: Ident { - value: "quantity".to_string(), - quote_style: None, - span: Span::empty() - }, + aggregate_functions: vec![ + ExprWithAlias { + expr: call("SUM", [Expr::Identifier(Ident::new("age"))]), + alias: Some(Ident::new("a")) + }, + ExprWithAlias { + expr: call("AVG", [Expr::Identifier(Ident::new("class"))]), + alias: Some(Ident::new("c")) + }, + ], + value_column: vec![ + Expr::Identifier(Ident::new("name")), + Expr::Identifier(Ident::new("age")), + ], + value_source: PivotValueSource::List(vec![ + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::Value( + (Value::SingleQuotedString("John".to_string())).with_empty_span() + ), + Expr::Value( + (Value::Number("30".parse().unwrap(), false)).with_empty_span() + ), + ]), + alias: Some(Ident::new("c1")) + }, + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::Value( + (Value::SingleQuotedString("Mike".to_string())).with_empty_span() + ), + Expr::Value( + (Value::Number("40".parse().unwrap(), false)).with_empty_span() + ), + ]), + alias: Some(Ident::new("c2")) + }, + ]), + default_on_null: None, + alias: None, + } + ); + assert_eq!( + verified_stmt(multiple_value_columns_sql).to_string(), + multiple_value_columns_sql + ); +} - name: Ident { - value: "quarter".to_string(), - quote_style: None, - span: Span::empty() - }, - columns: ["Q1", "Q2", "Q3", "Q4"] - .into_iter() - .map(Ident::new) - .collect(), +#[test] +fn parse_unpivot_table() { + let sql = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" + ); + let base_unpivot = Unpivot { + table: Box::new(TableFactor::Table { + name: ObjectName::from(vec![Ident::new("sales")]), alias: Some(TableAlias { - name: Ident::new("u"), - columns: ["product", "quarter", "quantity"] - .into_iter() - .map(TableAliasColumnDef::from_name) - .collect(), + name: Ident::new("s"), + columns: vec![], }), - } - ); + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }), + null_inclusion: None, + value: Expr::Identifier(Ident::new("quantity")), + name: Ident::new("quarter"), + columns: ["Q1", "Q2", "Q3", "Q4"] + .into_iter() + .map(|col| ExprWithAlias { + expr: Expr::Identifier(Ident::new(col)), + alias: None, + }) + .collect(), + alias: Some(TableAlias { + name: Ident::new("u"), + columns: ["product", "quarter", "quantity"] + .into_iter() + .map(TableAliasColumnDef::from_name) + .collect(), + }), + }; + pretty_assertions::assert_eq!(verified_only_select(sql).from[0].relation, base_unpivot); assert_eq!(verified_stmt(sql).to_string(), sql); let sql_without_aliases = concat!( @@ -10766,6 +11173,161 @@ fn parse_unpivot_table() { verified_stmt(sql_without_aliases).to_string(), sql_without_aliases ); + + let sql_unpivot_exclude_nulls = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT EXCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" + ); + + if let Unpivot { null_inclusion, .. } = + &verified_only_select(sql_unpivot_exclude_nulls).from[0].relation + { + assert_eq!(*null_inclusion, Some(NullInclusion::ExcludeNulls)); + } + + assert_eq!( + verified_stmt(sql_unpivot_exclude_nulls).to_string(), + sql_unpivot_exclude_nulls + ); + + let sql_unpivot_include_nulls = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" + ); + + if let Unpivot { null_inclusion, .. } = + &verified_only_select(sql_unpivot_include_nulls).from[0].relation + { + assert_eq!(*null_inclusion, Some(NullInclusion::IncludeNulls)); + } + + assert_eq!( + verified_stmt(sql_unpivot_include_nulls).to_string(), + sql_unpivot_include_nulls + ); + + let sql_unpivot_with_alias = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS ", + "(quantity FOR quarter IN ", + "(Q1 AS Quater1, Q2 AS Quater2, Q3 AS Quater3, Q4 AS Quater4)) ", + "AS u (product, quarter, quantity)" + ); + + if let Unpivot { value, columns, .. } = + &verified_only_select(sql_unpivot_with_alias).from[0].relation + { + assert_eq!( + *columns, + vec![ + ExprWithAlias { + expr: Expr::Identifier(Ident::new("Q1")), + alias: Some(Ident::new("Quater1")), + }, + ExprWithAlias { + expr: Expr::Identifier(Ident::new("Q2")), + alias: Some(Ident::new("Quater2")), + }, + ExprWithAlias { + expr: Expr::Identifier(Ident::new("Q3")), + alias: Some(Ident::new("Quater3")), + }, + ExprWithAlias { + expr: Expr::Identifier(Ident::new("Q4")), + alias: Some(Ident::new("Quater4")), + }, + ] + ); + assert_eq!(*value, Expr::Identifier(Ident::new("quantity"))); + } + + assert_eq!( + verified_stmt(sql_unpivot_with_alias).to_string(), + sql_unpivot_with_alias + ); + + let sql_unpivot_with_alias_and_multi_value = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS ((first_quarter, second_quarter) ", + "FOR half_of_the_year IN (", + "(Q1, Q2) AS H1, ", + "(Q3, Q4) AS H2", + "))" + ); + + if let Unpivot { value, columns, .. } = + &verified_only_select(sql_unpivot_with_alias_and_multi_value).from[0].relation + { + assert_eq!( + *columns, + vec![ + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::Identifier(Ident::new("Q1")), + Expr::Identifier(Ident::new("Q2")), + ]), + alias: Some(Ident::new("H1")), + }, + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::Identifier(Ident::new("Q3")), + Expr::Identifier(Ident::new("Q4")), + ]), + alias: Some(Ident::new("H2")), + }, + ] + ); + assert_eq!( + *value, + Expr::Tuple(vec![ + Expr::Identifier(Ident::new("first_quarter")), + Expr::Identifier(Ident::new("second_quarter")), + ]) + ); + } + + assert_eq!( + verified_stmt(sql_unpivot_with_alias_and_multi_value).to_string(), + sql_unpivot_with_alias_and_multi_value + ); + + let sql_unpivot_with_alias_and_multi_value_and_qualifier = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS ((first_quarter, second_quarter) ", + "FOR half_of_the_year IN (", + "(sales.Q1, sales.Q2) AS H1, ", + "(sales.Q3, sales.Q4) AS H2", + "))" + ); + + if let Unpivot { columns, .. } = + &verified_only_select(sql_unpivot_with_alias_and_multi_value_and_qualifier).from[0].relation + { + assert_eq!( + *columns, + vec![ + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::CompoundIdentifier(vec![Ident::new("sales"), Ident::new("Q1"),]), + Expr::CompoundIdentifier(vec![Ident::new("sales"), Ident::new("Q2"),]), + ]), + alias: Some(Ident::new("H1")), + }, + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::CompoundIdentifier(vec![Ident::new("sales"), Ident::new("Q3"),]), + Expr::CompoundIdentifier(vec![Ident::new("sales"), Ident::new("Q4"),]), + ]), + alias: Some(Ident::new("H2")), + }, + ] + ); + } + + assert_eq!( + verified_stmt(sql_unpivot_with_alias_and_multi_value_and_qualifier).to_string(), + sql_unpivot_with_alias_and_multi_value_and_qualifier + ); } #[test] @@ -10862,20 +11424,15 @@ fn parse_pivot_unpivot_table() { sample: None, index_hints: vec![], }), - value: Ident { - value: "population".to_string(), - quote_style: None, - span: Span::empty() - }, - - name: Ident { - value: "year".to_string(), - quote_style: None, - span: Span::empty() - }, + null_inclusion: None, + value: Expr::Identifier(Ident::new("population")), + name: Ident::new("year"), columns: ["population_2000", "population_2010"] .into_iter() - .map(Ident::new) + .map(|col| ExprWithAlias { + expr: Expr::Identifier(Ident::new(col)), + alias: None, + }) .collect(), alias: Some(TableAlias { name: Ident::new("u"), @@ -10886,7 +11443,7 @@ fn parse_pivot_unpivot_table() { expr: call("sum", [Expr::Identifier(Ident::new("population"))]), alias: None }], - value_column: vec![Ident::new("year")], + value_column: vec![Expr::Identifier(Ident::new("year"))], value_source: PivotValueSource::List(vec![ ExprWithAlias { expr: Expr::Value( @@ -10940,10 +11497,15 @@ fn parse_non_latin_identifiers() { Box::new(RedshiftSqlDialect {}), Box::new(MySqlDialect {}), ]); - supported_dialects.verified_stmt("SELECT a.説明 FROM test.public.inter01 AS a"); supported_dialects.verified_stmt("SELECT a.説明 FROM inter01 AS a, inter01_transactions AS b WHERE a.説明 = b.取引 GROUP BY a.説明"); supported_dialects.verified_stmt("SELECT 説明, hühnervögel, garçon, Москва, 東京 FROM inter01"); + + let supported_dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + Box::new(MsSqlDialect {}), + ]); assert!(supported_dialects .parse_sql_statements("SELECT 💝 FROM table1") .is_err()); @@ -10992,7 +11554,7 @@ fn parse_trailing_comma() { trailing_commas.verified_stmt(r#"SELECT "from" FROM "from""#); // doesn't allow any trailing commas - let trailing_commas = TestedDialects::new(vec![Box::new(GenericDialect {})]); + let trailing_commas = TestedDialects::new(vec![Box::new(PostgreSqlDialect {})]); assert_eq!( trailing_commas @@ -11182,6 +11744,8 @@ fn parse_execute_stored_procedure() { immediate: false, using: vec![], into: vec![], + output: false, + default: false, }; assert_eq!( // Microsoft SQL Server does not use parentheses around arguments for EXECUTE @@ -11196,6 +11760,18 @@ fn parse_execute_stored_procedure() { ), expected ); + match ms_and_generic().verified_stmt("EXECUTE dbo.proc1 @ReturnVal = @X OUTPUT") { + Statement::Execute { output, .. } => { + assert!(output); + } + _ => unreachable!(), + } + match ms_and_generic().verified_stmt("EXECUTE dbo.proc1 DEFAULT") { + Statement::Execute { default, .. } => { + assert!(default); + } + _ => unreachable!(), + } } #[test] @@ -11214,6 +11790,8 @@ fn parse_execute_immediate() { into: vec![Ident::new("a")], name: None, has_parentheses: false, + output: false, + default: false, }; let stmt = dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1' INTO a USING 1 AS b"); @@ -11316,6 +11894,7 @@ fn parse_unload() { top: None, top_before_distinct: false, projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("tab")])), @@ -11344,6 +11923,7 @@ fn parse_unload() { order_by: None, settings: None, format_clause: None, + pipe_operators: vec![], }), to: Ident { value: "s3://...".to_string(), @@ -11414,7 +11994,9 @@ fn test_parse_inline_comment() { // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) match all_dialects_except(|d| d.is::()).verified_stmt(sql) { Statement::CreateTable(CreateTable { - columns, comment, .. + columns, + table_options, + .. }) => { assert_eq!( columns, @@ -11428,8 +12010,10 @@ fn test_parse_inline_comment() { }] ); assert_eq!( - comment.unwrap(), - CommentDef::WithEq("comment with equal".to_string()) + table_options, + CreateTableOptions::Plain(vec![SqlOption::Comment(CommentDef::WithEq( + "comment with equal".to_string() + ))]) ); } _ => unreachable!(), @@ -11511,6 +12095,7 @@ fn parse_connect_by() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), ], + exclude: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), joins: vec![], @@ -11592,6 +12177,7 @@ fn parse_connect_by() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), ], + exclude: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), joins: vec![], @@ -12454,21 +13040,6 @@ fn parse_select_wildcard_with_except() { ); } -#[test] -fn parse_auto_increment_too_large() { - let dialect = GenericDialect {}; - let u64_max = u64::MAX; - let sql = - format!("CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) AUTO_INCREMENT=1{u64_max}"); - - let res = Parser::new(&dialect) - .try_with_sql(&sql) - .expect("tokenize to work") - .parse_statements(); - - assert!(res.is_err(), "{res:?}"); -} - #[test] fn test_group_by_nothing() { let Select { group_by, .. } = all_dialects_where(|d| d.supports_group_by_expr()) @@ -12511,7 +13082,10 @@ fn test_extract_seconds_ok() { expr: Box::new(Expr::Value( (Value::SingleQuotedString("2 seconds".to_string())).with_empty_span() )), - data_type: DataType::Interval, + data_type: DataType::Interval { + fields: None, + precision: None + }, format: None, }), } @@ -12536,10 +13110,14 @@ fn test_extract_seconds_ok() { expr: Box::new(Expr::Value( (Value::SingleQuotedString("2 seconds".to_string())).with_empty_span(), )), - data_type: DataType::Interval, + data_type: DataType::Interval { + fields: None, + precision: None, + }, format: None, }), })], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -12564,6 +13142,7 @@ fn test_extract_seconds_ok() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }))]; assert_eq!(actual_ast, expected_ast); @@ -12588,7 +13167,10 @@ fn test_extract_seconds_single_quote_ok() { expr: Box::new(Expr::Value( (Value::SingleQuotedString("2 seconds".to_string())).with_empty_span() )), - data_type: DataType::Interval, + data_type: DataType::Interval { + fields: None, + precision: None + }, format: None, }), } @@ -14189,7 +14771,7 @@ fn overflow() { let expr = std::iter::repeat_n("1", 1000) .collect::>() .join(" + "); - let sql = format!("SELECT {}", expr); + let sql = format!("SELECT {expr}"); let mut statements = Parser::parse_sql(&GenericDialect {}, sql.as_str()).unwrap(); let statement = statements.pop().unwrap(); @@ -14282,7 +14864,7 @@ fn test_visit_order() { let sql = "SELECT CASE a WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE 5 END"; let stmt = verified_stmt(sql); let mut visited = vec![]; - sqlparser::ast::visit_expressions(&stmt, |expr| { + let _ = sqlparser::ast::visit_expressions(&stmt, |expr| { visited.push(expr.to_string()); core::ops::ControlFlow::<()>::Continue(()) }); @@ -14489,7 +15071,7 @@ fn test_conditional_statement_span() { else_block.unwrap().span() ); } - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } } @@ -14545,6 +15127,8 @@ fn test_lambdas() { Expr::Lambda(LambdaFunction { params: OneOrManyWithParens::Many(vec![Ident::new("p1"), Ident::new("p2")]), body: Box::new(Expr::Case { + case_token: AttachedToken::empty(), + end_token: AttachedToken::empty(), operand: None, conditions: vec![ CaseWhen { @@ -14609,6 +15193,7 @@ fn test_select_from_first() { distinct: None, top: None, projection, + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { @@ -14641,6 +15226,7 @@ fn test_select_from_first() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }; assert_eq!(expected, ast); assert_eq!(ast.to_string(), q); @@ -14704,62 +15290,90 @@ fn test_geometry_type() { let sql = "point '1,2'"; assert_eq!( all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Point), - value: Value::SingleQuotedString("1,2".to_string()), - } + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2".to_string()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) ); let sql = "line '1,2,3,4'"; assert_eq!( all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Line), - value: Value::SingleQuotedString("1,2,3,4".to_string()), - } + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) ); let sql = "path '1,2,3,4'"; assert_eq!( all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::GeometricType(GeometricTypeKind::GeometricPath), - value: Value::SingleQuotedString("1,2,3,4".to_string()), - } + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) ); let sql = "box '1,2,3,4'"; assert_eq!( all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::GeometricType(GeometricTypeKind::GeometricBox), - value: Value::SingleQuotedString("1,2,3,4".to_string()), - } + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) ); let sql = "circle '1,2,3'"; assert_eq!( all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Circle), - value: Value::SingleQuotedString("1,2,3".to_string()), - } + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3".to_string()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) ); let sql = "polygon '1,2,3,4'"; assert_eq!( all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Polygon), - value: Value::SingleQuotedString("1,2,3,4".to_string()), - } + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) ); let sql = "lseg '1,2,3,4'"; assert_eq!( all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::GeometricType(GeometricTypeKind::LineSegment), - value: Value::SingleQuotedString("1,2,3,4".to_string()), - } + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) ); } #[test] @@ -15021,66 +15635,563 @@ fn parse_set_names() { } #[test] -fn parse_multiple_set_statements() -> Result<(), ParserError> { - let dialects = all_dialects_where(|d| d.supports_comma_separated_set_assignments()); - let stmt = dialects.verified_stmt("SET @a = 1, b = 2"); +fn parse_pipeline_operator() { + let dialects = all_dialects_where(|d| d.supports_pipe_operator()); - match stmt { - Statement::Set(Set::MultipleAssignments { assignments }) => { - assert_eq!( - assignments, - vec![ - SetAssignment { - scope: None, - name: ObjectName::from(vec!["@a".into()]), - value: Expr::value(number("1")) - }, - SetAssignment { - scope: None, - name: ObjectName::from(vec!["b".into()]), - value: Expr::value(number("2")) - } - ] - ); - } - _ => panic!("Expected SetVariable with 2 variables and 2 values"), - }; + // select pipe operator + dialects.verified_stmt("SELECT * FROM users |> SELECT id"); + dialects.verified_stmt("SELECT * FROM users |> SELECT id, name"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> SELECT id user_id", + "SELECT * FROM users |> SELECT id AS user_id", + ); + dialects.verified_stmt("SELECT * FROM users |> SELECT id AS user_id"); - let stmt = dialects.verified_stmt("SET GLOBAL @a = 1, SESSION b = 2, LOCAL c = 3, d = 4"); + // extend pipe operator + dialects.verified_stmt("SELECT * FROM users |> EXTEND id + 1 AS new_id"); + dialects.verified_stmt("SELECT * FROM users |> EXTEND id AS new_id, name AS new_name"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> EXTEND id user_id", + "SELECT * FROM users |> EXTEND id AS user_id", + ); - match stmt { - Statement::Set(Set::MultipleAssignments { assignments }) => { - assert_eq!( - assignments, - vec![ - SetAssignment { - scope: Some(ContextModifier::Global), - name: ObjectName::from(vec!["@a".into()]), - value: Expr::value(number("1")) - }, - SetAssignment { - scope: Some(ContextModifier::Session), - name: ObjectName::from(vec!["b".into()]), - value: Expr::value(number("2")) - }, - SetAssignment { - scope: Some(ContextModifier::Local), - name: ObjectName::from(vec!["c".into()]), - value: Expr::value(number("3")) - }, - SetAssignment { - scope: None, - name: ObjectName::from(vec!["d".into()]), - value: Expr::value(number("4")) - } - ] - ); - } - _ => panic!("Expected MultipleAssignments with 4 scoped variables and 4 values"), - }; + // set pipe operator + dialects.verified_stmt("SELECT * FROM users |> SET id = id + 1"); + dialects.verified_stmt("SELECT * FROM users |> SET id = id + 1, name = name + ' Doe'"); - Ok(()) -} + // drop pipe operator + dialects.verified_stmt("SELECT * FROM users |> DROP id"); + dialects.verified_stmt("SELECT * FROM users |> DROP id, name"); + + // as pipe operator + dialects.verified_stmt("SELECT * FROM users |> AS new_users"); + + // limit pipe operator + dialects.verified_stmt("SELECT * FROM users |> LIMIT 10"); + dialects.verified_stmt("SELECT * FROM users |> LIMIT 10 OFFSET 5"); + dialects.verified_stmt("SELECT * FROM users |> LIMIT 10 |> LIMIT 5"); + dialects.verified_stmt("SELECT * FROM users |> LIMIT 10 |> WHERE true"); + + // where pipe operator + dialects.verified_stmt("SELECT * FROM users |> WHERE id = 1"); + dialects.verified_stmt("SELECT * FROM users |> WHERE id = 1 AND name = 'John'"); + dialects.verified_stmt("SELECT * FROM users |> WHERE id = 1 OR name = 'John'"); + + // aggregate pipe operator full table + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE COUNT(*)"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> AGGREGATE COUNT(*) total_users", + "SELECT * FROM users |> AGGREGATE COUNT(*) AS total_users", + ); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE COUNT(*) AS total_users"); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE COUNT(*), MIN(id)"); + + // aggregate pipe opeprator with grouping + dialects.verified_stmt( + "SELECT * FROM users |> AGGREGATE SUM(o_totalprice) AS price, COUNT(*) AS cnt GROUP BY EXTRACT(YEAR FROM o_orderdate) AS year", + ); + dialects.verified_stmt( + "SELECT * FROM users |> AGGREGATE GROUP BY EXTRACT(YEAR FROM o_orderdate) AS year", + ); + dialects + .verified_stmt("SELECT * FROM users |> AGGREGATE GROUP BY EXTRACT(YEAR FROM o_orderdate)"); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE GROUP BY a, b"); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE SUM(c) GROUP BY a, b"); + dialects.verified_stmt("SELECT * FROM users |> AGGREGATE SUM(c) ASC"); + + // order by pipe operator + dialects.verified_stmt("SELECT * FROM users |> ORDER BY id ASC"); + dialects.verified_stmt("SELECT * FROM users |> ORDER BY id DESC"); + dialects.verified_stmt("SELECT * FROM users |> ORDER BY id DESC, name ASC"); + + // tablesample pipe operator + dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE BERNOULLI (50)"); + dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50 PERCENT)"); + dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50) REPEATABLE (10)"); + + // rename pipe operator + dialects.verified_stmt("SELECT * FROM users |> RENAME old_name AS new_name"); + dialects.verified_stmt("SELECT * FROM users |> RENAME id AS user_id, name AS user_name"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> RENAME id user_id", + "SELECT * FROM users |> RENAME id AS user_id", + ); + + // union pipe operator + dialects.verified_stmt("SELECT * FROM users |> UNION ALL (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION (SELECT * FROM admins)"); + + // union pipe operator with multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> UNION ALL (SELECT * FROM admins), (SELECT * FROM guests)", + ); + dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins), (SELECT * FROM guests), (SELECT * FROM employees)"); + dialects.verified_stmt( + "SELECT * FROM users |> UNION (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // union pipe operator with BY NAME modifier + dialects.verified_stmt("SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION ALL BY NAME (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT BY NAME (SELECT * FROM admins)"); + + // union pipe operator with BY NAME and multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // intersect pipe operator (BigQuery requires DISTINCT modifier for INTERSECT) + dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins)"); + + // intersect pipe operator with BY NAME modifier + dialects + .verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins)"); + + // intersect pipe operator with multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // intersect pipe operator with BY NAME and multiple queries + dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)"); + + // except pipe operator (BigQuery requires DISTINCT modifier for EXCEPT) + dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins)"); + + // except pipe operator with BY NAME modifier + dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins)"); + + // except pipe operator with multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // except pipe operator with BY NAME and multiple queries + dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)"); + + // call pipe operator + dialects.verified_stmt("SELECT * FROM users |> CALL my_function()"); + dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5, 'test')"); + dialects.verified_stmt( + "SELECT * FROM users |> CALL namespace.function_name(col1, col2, 'literal')", + ); + + // call pipe operator with complex arguments + dialects.verified_stmt("SELECT * FROM users |> CALL transform_data(col1 + col2)"); + dialects.verified_stmt("SELECT * FROM users |> CALL analyze_data('param1', 100, true)"); + + // call pipe operator with aliases + dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) AS al"); + dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5) AS result_table"); + dialects.verified_stmt("SELECT * FROM users |> CALL namespace.func() AS my_alias"); + + // multiple call pipe operators in sequence + dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) |> CALL tvf2(arg2, arg3)"); + dialects.verified_stmt( + "SELECT * FROM data |> CALL transform(col1) |> CALL validate() |> CALL process(param)", + ); + + // multiple call pipe operators with aliases + dialects.verified_stmt( + "SELECT * FROM input_table |> CALL tvf1(arg1) AS step1 |> CALL tvf2(arg2) AS step2", + ); + dialects.verified_stmt( + "SELECT * FROM data |> CALL preprocess() AS clean_data |> CALL analyze(mode) AS results", + ); + + // call pipe operators mixed with other pipe operators + dialects.verified_stmt( + "SELECT * FROM users |> CALL transform() |> WHERE status = 'active' |> CALL process(param)", + ); + dialects.verified_stmt( + "SELECT * FROM data |> CALL preprocess() AS clean |> SELECT col1, col2 |> CALL validate()", + ); + + // pivot pipe operator + dialects.verified_stmt( + "SELECT * FROM monthly_sales |> PIVOT(SUM(amount) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))", + ); + dialects.verified_stmt("SELECT * FROM sales_data |> PIVOT(AVG(revenue) FOR region IN ('North', 'South', 'East', 'West'))"); + + // pivot pipe operator with multiple aggregate functions + dialects.verified_stmt("SELECT * FROM data |> PIVOT(SUM(sales) AS total_sales, COUNT(*) AS num_transactions FOR month IN ('Jan', 'Feb', 'Mar'))"); + + // pivot pipe operator with compound column names + dialects.verified_stmt("SELECT * FROM sales |> PIVOT(SUM(amount) FOR product.category IN ('Electronics', 'Clothing'))"); + + // pivot pipe operator mixed with other pipe operators + dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> PIVOT(SUM(revenue) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))"); + + // pivot pipe operator with aliases + dialects.verified_stmt("SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales"); + dialects.verified_stmt("SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category"); + dialects.verified_stmt("SELECT * FROM sales |> PIVOT(COUNT(*) AS transactions, SUM(amount) AS total FOR region IN ('North', 'South')) AS regional_summary"); + + // pivot pipe operator with implicit aliases (without AS keyword) + dialects.verified_query_with_canonical( + "SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) quarterly_sales", + "SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales", + ); + dialects.verified_query_with_canonical( + "SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) avg_by_category", + "SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category", + ); + + // unpivot pipe operator basic usage + dialects + .verified_stmt("SELECT * FROM sales |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))"); + dialects.verified_stmt("SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C))"); + dialects.verified_stmt( + "SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory, disk))", + ); + + // unpivot pipe operator with multiple columns + dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (jan, feb, mar, apr, may, jun))"); + dialects.verified_stmt( + "SELECT * FROM report |> UNPIVOT(score FOR subject IN (math, science, english, history))", + ); + + // unpivot pipe operator mixed with other pipe operators + dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))"); + + // unpivot pipe operator with aliases + dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales"); + dialects.verified_stmt( + "SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data", + ); + dialects.verified_stmt("SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory)) AS metric_measurements"); + + // unpivot pipe operator with implicit aliases (without AS keyword) + dialects.verified_query_with_canonical( + "SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) unpivoted_sales", + "SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales", + ); + dialects.verified_query_with_canonical( + "SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) transformed_data", + "SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data", + ); + + // many pipes + dialects.verified_stmt( + "SELECT * FROM CustomerOrders |> AGGREGATE SUM(cost) AS total_cost GROUP BY customer_id, state, item_type |> EXTEND COUNT(*) OVER (PARTITION BY customer_id) AS num_orders |> WHERE num_orders > 1 |> AGGREGATE AVG(total_cost) AS average GROUP BY state DESC, item_type ASC", + ); + + // join pipe operator - INNER JOIN + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt("SELECT * FROM users |> INNER JOIN orders ON users.id = orders.user_id"); + + // join pipe operator - LEFT JOIN + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt( + "SELECT * FROM users |> LEFT OUTER JOIN orders ON users.id = orders.user_id", + ); + + // join pipe operator - RIGHT JOIN + dialects.verified_stmt("SELECT * FROM users |> RIGHT JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt( + "SELECT * FROM users |> RIGHT OUTER JOIN orders ON users.id = orders.user_id", + ); + + // join pipe operator - FULL JOIN + dialects.verified_stmt("SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> FULL OUTER JOIN orders ON users.id = orders.user_id", + "SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id", + ); + + // join pipe operator - CROSS JOIN + dialects.verified_stmt("SELECT * FROM users |> CROSS JOIN orders"); + + // join pipe operator with USING + dialects.verified_query_with_canonical( + "SELECT * FROM users |> JOIN orders USING (user_id)", + "SELECT * FROM users |> JOIN orders USING(user_id)", + ); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> LEFT JOIN orders USING (user_id, order_date)", + "SELECT * FROM users |> LEFT JOIN orders USING(user_id, order_date)", + ); + + // join pipe operator with alias + dialects.verified_query_with_canonical( + "SELECT * FROM users |> JOIN orders o ON users.id = o.user_id", + "SELECT * FROM users |> JOIN orders AS o ON users.id = o.user_id", + ); + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders AS o ON users.id = o.user_id"); + + // join pipe operator with complex ON condition + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id AND orders.status = 'active'"); + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id AND orders.amount > 100"); + + // multiple join pipe operators + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> JOIN products ON orders.product_id = products.id"); + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id |> RIGHT JOIN products ON orders.product_id = products.id"); + + // join pipe operator with other pipe operators + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> WHERE orders.amount > 100"); + dialects.verified_stmt("SELECT * FROM users |> WHERE users.active = true |> LEFT JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> SELECT users.name, orders.amount"); +} + +#[test] +fn parse_pipeline_operator_negative_tests() { + let dialects = all_dialects_where(|d| d.supports_pipe_operator()); + + // Test that plain EXCEPT without DISTINCT fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> EXCEPT (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that EXCEPT ALL fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> EXCEPT ALL (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that EXCEPT BY NAME without DISTINCT fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> EXCEPT BY NAME (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that EXCEPT ALL BY NAME fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements( + "SELECT * FROM users |> EXCEPT ALL BY NAME (SELECT * FROM admins)" + ) + .unwrap_err() + ); + + // Test that plain INTERSECT without DISTINCT fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> INTERSECT (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that INTERSECT ALL fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> INTERSECT ALL (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that INTERSECT BY NAME without DISTINCT fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> INTERSECT BY NAME (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that INTERSECT ALL BY NAME fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements( + "SELECT * FROM users |> INTERSECT ALL BY NAME (SELECT * FROM admins)" + ) + .unwrap_err() + ); + + // Test that CALL without function name fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL") + .is_err()); + + // Test that CALL without parentheses fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL my_function") + .is_err()); + + // Test that CALL with invalid function syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL 123invalid") + .is_err()); + + // Test that CALL with malformed arguments fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL my_function(,)") + .is_err()); + + // Test that CALL with invalid alias syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL my_function() AS") + .is_err()); + + // Test that PIVOT without parentheses fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT SUM(amount) FOR month IN ('Jan')") + .is_err()); + + // Test that PIVOT without FOR keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) month IN ('Jan'))") + .is_err()); + + // Test that PIVOT without IN keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month ('Jan'))") + .is_err()); + + // Test that PIVOT with empty IN list fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ())") + .is_err()); + + // Test that PIVOT with invalid alias syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ('Jan')) AS") + .is_err()); + + // Test UNPIVOT negative cases + + // Test that UNPIVOT without parentheses fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT value FOR name IN col1, col2") + .is_err()); + + // Test that UNPIVOT without FOR keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value name IN (col1, col2))") + .is_err()); + + // Test that UNPIVOT without IN keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name (col1, col2))") + .is_err()); + + // Test that UNPIVOT with missing value column fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(FOR name IN (col1, col2))") + .is_err()); + + // Test that UNPIVOT with missing name column fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR IN (col1, col2))") + .is_err()); + + // Test that UNPIVOT with empty IN list fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN ())") + .is_err()); + + // Test that UNPIVOT with invalid alias syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)) AS") + .is_err()); + + // Test that UNPIVOT with missing closing parenthesis fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)") + .is_err()); + + // Test that JOIN without table name fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN ON users.id = orders.user_id") + .is_err()); + + // Test that CROSS JOIN with ON condition fails + assert!(dialects + .parse_sql_statements( + "SELECT * FROM users |> CROSS JOIN orders ON users.id = orders.user_id" + ) + .is_err()); + + // Test that CROSS JOIN with USING condition fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CROSS JOIN orders USING (user_id)") + .is_err()); + + // Test that JOIN with empty USING list fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN orders USING ()") + .is_err()); + + // Test that JOIN with malformed ON condition fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN orders ON") + .is_err()); + + // Test that JOIN with invalid USING syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN orders USING user_id") + .is_err()); +} + +#[test] +fn parse_multiple_set_statements() -> Result<(), ParserError> { + let dialects = all_dialects_where(|d| d.supports_comma_separated_set_assignments()); + let stmt = dialects.verified_stmt("SET @a = 1, b = 2"); + + match stmt { + Statement::Set(Set::MultipleAssignments { assignments }) => { + assert_eq!( + assignments, + vec![ + SetAssignment { + scope: None, + name: ObjectName::from(vec!["@a".into()]), + value: Expr::value(number("1")) + }, + SetAssignment { + scope: None, + name: ObjectName::from(vec!["b".into()]), + value: Expr::value(number("2")) + } + ] + ); + } + _ => panic!("Expected SetVariable with 2 variables and 2 values"), + }; + + let stmt = dialects.verified_stmt("SET GLOBAL @a = 1, SESSION b = 2, LOCAL c = 3, d = 4"); + + match stmt { + Statement::Set(Set::MultipleAssignments { assignments }) => { + assert_eq!( + assignments, + vec![ + SetAssignment { + scope: Some(ContextModifier::Global), + name: ObjectName::from(vec!["@a".into()]), + value: Expr::value(number("1")) + }, + SetAssignment { + scope: Some(ContextModifier::Session), + name: ObjectName::from(vec!["b".into()]), + value: Expr::value(number("2")) + }, + SetAssignment { + scope: Some(ContextModifier::Local), + name: ObjectName::from(vec!["c".into()]), + value: Expr::value(number("3")) + }, + SetAssignment { + scope: None, + name: ObjectName::from(vec!["d".into()]), + value: Expr::value(number("4")) + } + ] + ); + } + _ => panic!("Expected MultipleAssignments with 4 scoped variables and 4 values"), + }; + + Ok(()) +} #[test] fn parse_set_time_zone_alias() { @@ -15103,3 +16214,816 @@ fn parse_return() { let _ = all_dialects().verified_stmt("RETURN 1"); } + +#[test] +fn parse_subquery_limit() { + let _ = all_dialects().verified_stmt("SELECT t1_id, t1_name FROM t1 WHERE t1_id IN (SELECT t2_id FROM t2 WHERE t1_name = t2_name LIMIT 10)"); +} + +#[test] +fn test_open() { + let open_cursor = "OPEN Employee_Cursor"; + let stmt = all_dialects().verified_stmt(open_cursor); + assert_eq!( + stmt, + Statement::Open(OpenStatement { + cursor_name: Ident::new("Employee_Cursor"), + }) + ); +} + +#[test] +fn parse_truncate_only() { + let truncate = all_dialects().verified_stmt("TRUNCATE TABLE employee, ONLY dept"); + + let table_names = vec![ + TruncateTableTarget { + name: ObjectName::from(vec![Ident::new("employee")]), + only: false, + }, + TruncateTableTarget { + name: ObjectName::from(vec![Ident::new("dept")]), + only: true, + }, + ]; + + assert_eq!( + Statement::Truncate { + table_names, + partitions: None, + table: true, + identity: None, + cascade: None, + on_cluster: None, + }, + truncate + ); +} + +#[test] +fn check_enforced() { + all_dialects().verified_stmt( + "CREATE TABLE t (a INT, b INT, c INT, CHECK (a > 0) NOT ENFORCED, CHECK (b > 0) ENFORCED, CHECK (c > 0))", + ); +} + +#[test] +fn join_precedence() { + all_dialects_except(|d| !d.supports_left_associative_joins_without_parens()) + .verified_query_with_canonical( + "SELECT * + FROM t1 + NATURAL JOIN t5 + INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 + WHERE t0.v1 = t1.v0", + // canonical string without parentheses + "SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 WHERE t0.v1 = t1.v0", + ); + all_dialects_except(|d| d.supports_left_associative_joins_without_parens()).verified_query_with_canonical( + "SELECT * + FROM t1 + NATURAL JOIN t5 + INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 + WHERE t0.v1 = t1.v0", + // canonical string with parentheses + "SELECT * FROM t1 NATURAL JOIN (t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0) WHERE t0.v1 = t1.v0", + ); +} + +#[test] +fn parse_create_procedure_with_language() { + let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; END"#; + match verified_stmt(sql) { + Statement::CreateProcedure { + or_alter, + name, + params, + language, + .. + } => { + assert_eq!(or_alter, false); + assert_eq!(name.to_string(), "test_proc"); + assert_eq!(params, Some(vec![])); + assert_eq!( + language, + Some(Ident { + value: "sql".into(), + quote_style: None, + span: Span { + start: Location::empty(), + end: Location::empty() + } + }) + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_procedure_with_parameter_modes() { + let sql = r#"CREATE PROCEDURE test_proc (IN a INTEGER, OUT b TEXT, INOUT c TIMESTAMP, d BOOL) AS BEGIN SELECT 1; END"#; + match verified_stmt(sql) { + Statement::CreateProcedure { + or_alter, + name, + params, + .. + } => { + assert_eq!(or_alter, false); + assert_eq!(name.to_string(), "test_proc"); + let fake_span = Span { + start: Location { line: 0, column: 0 }, + end: Location { line: 0, column: 0 }, + }; + assert_eq!( + params, + Some(vec![ + ProcedureParam { + name: Ident { + value: "a".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Integer(None), + mode: Some(ArgMode::In) + }, + ProcedureParam { + name: Ident { + value: "b".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Text, + mode: Some(ArgMode::Out) + }, + ProcedureParam { + name: Ident { + value: "c".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Timestamp(None, TimezoneInfo::None), + mode: Some(ArgMode::InOut) + }, + ProcedureParam { + name: Ident { + value: "d".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Bool, + mode: None + }, + ]) + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_not_null() { + let _ = all_dialects().expr_parses_to("x NOT NULL", "x IS NOT NULL"); + let _ = all_dialects().expr_parses_to("NULL NOT NULL", "NULL IS NOT NULL"); + + assert_matches!( + all_dialects().expr_parses_to("NOT NULL NOT NULL", "NOT NULL IS NOT NULL"), + Expr::UnaryOp { + op: UnaryOperator::Not, + .. + } + ); + assert_matches!( + all_dialects().expr_parses_to("NOT x NOT NULL", "NOT x IS NOT NULL"), + Expr::UnaryOp { + op: UnaryOperator::Not, + .. + } + ); +} + +#[test] +fn test_select_exclude() { + let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude()); + match &dialects + .verified_only_select("SELECT * EXCLUDE c1 FROM test") + .projection[0] + { + SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { + assert_eq!( + *opt_exclude, + Some(ExcludeSelectItem::Single(Ident::new("c1"))) + ); + } + _ => unreachable!(), + } + match &dialects + .verified_only_select("SELECT * EXCLUDE (c1, c2) FROM test") + .projection[0] + { + SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { + assert_eq!( + *opt_exclude, + Some(ExcludeSelectItem::Multiple(vec![ + Ident::new("c1"), + Ident::new("c2") + ])) + ); + } + _ => unreachable!(), + } + let select = dialects.verified_only_select("SELECT * EXCLUDE c1, c2 FROM test"); + match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { + assert_eq!( + *opt_exclude, + Some(ExcludeSelectItem::Single(Ident::new("c1"))) + ); + } + _ => unreachable!(), + } + match &select.projection[1] { + SelectItem::UnnamedExpr(Expr::Identifier(ident)) => { + assert_eq!(*ident, Ident::new("c2")); + } + _ => unreachable!(), + } + + let dialects = all_dialects_where(|d| d.supports_select_exclude()); + let select = dialects.verified_only_select("SELECT *, c1 EXCLUDE c1 FROM test"); + match &select.projection[0] { + SelectItem::Wildcard(additional_options) => { + assert_eq!(*additional_options, WildcardAdditionalOptions::default()); + } + _ => unreachable!(), + } + assert_eq!( + select.exclude, + Some(ExcludeSelectItem::Single(Ident::new("c1"))) + ); + + let dialects = all_dialects_where(|d| { + d.supports_select_wildcard_exclude() && !d.supports_select_exclude() + }); + let select = dialects.verified_only_select("SELECT * EXCLUDE c1 FROM test"); + match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { + assert_eq!( + *opt_exclude, + Some(ExcludeSelectItem::Single(Ident::new("c1"))) + ); + } + _ => unreachable!(), + } + + // Dialects that only support the wildcard form and do not accept EXCLUDE as an implicity alias + // will fail when encountered with the `c2` ident + let dialects = all_dialects_where(|d| { + d.supports_select_wildcard_exclude() + && !d.supports_select_exclude() + && d.is_column_alias(&Keyword::EXCLUDE, &mut Parser::new(d)) + }); + assert_eq!( + dialects + .parse_sql_statements("SELECT *, c1 EXCLUDE c2 FROM test") + .err() + .unwrap(), + ParserError::ParserError("Expected: end of statement, found: c2".to_string()) + ); + + // Dialects that only support the wildcard form and accept EXCLUDE as an implicity alias + // will fail when encountered with the `EXCLUDE` keyword + let dialects = all_dialects_where(|d| { + d.supports_select_wildcard_exclude() + && !d.supports_select_exclude() + && !d.is_column_alias(&Keyword::EXCLUDE, &mut Parser::new(d)) + }); + assert_eq!( + dialects + .parse_sql_statements("SELECT *, c1 EXCLUDE c2 FROM test") + .err() + .unwrap(), + ParserError::ParserError("Expected: end of statement, found: EXCLUDE".to_string()) + ); +} + +#[test] +fn test_no_semicolon_required_between_statements() { + let sql = r#" +SELECT * FROM tbl1 +SELECT * FROM tbl2 + "#; + + let dialects = all_dialects_with_options(ParserOptions { + trailing_commas: false, + unescape: true, + require_semicolon_stmt_delimiter: false, + }); + let stmts = dialects.parse_sql_statements(sql).unwrap(); + assert_eq!(stmts.len(), 2); + assert!(stmts.iter().all(|s| matches!(s, Statement::Query { .. }))); +} + +#[test] +fn test_identifier_unicode_support() { + let sql = r#"SELECT phoneǤЖשचᎯ⻩☯♜🦄⚛🀄ᚠ⌛🌀 AS tbl FROM customers"#; + let dialects = TestedDialects::new(vec![ + Box::new(MySqlDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(PostgreSqlDialect {}), + ]); + let _ = dialects.verified_stmt(sql); +} + +#[test] +fn test_identifier_unicode_start() { + let sql = r#"SELECT 💝phone AS 💝 FROM customers"#; + let dialects = TestedDialects::new(vec![ + Box::new(MySqlDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(PostgreSqlDialect {}), + ]); + let _ = dialects.verified_stmt(sql); +} + +#[test] +fn parse_notnull() { + // Some dialects support `x NOTNULL` as an expression while others consider + // `x NOTNULL` like `x AS NOTNULL` and thus consider `NOTNULL` an alias for x. + let notnull_unsupported_dialects = all_dialects_except(|d| d.supports_notnull_operator()); + let _ = notnull_unsupported_dialects + .verified_only_select_with_canonical("SELECT NULL NOTNULL", "SELECT NULL AS NOTNULL"); + + // Supported dialects consider `x NOTNULL` as an alias for `x IS NOT NULL` + let notnull_supported_dialects = all_dialects_where(|d| d.supports_notnull_operator()); + let _ = notnull_supported_dialects.expr_parses_to("x NOTNULL", "x IS NOT NULL"); + + // For dialects which support it, `NOT NULL NOTNULL` should + // parse as `(NOT (NULL IS NOT NULL))` + assert_matches!( + notnull_supported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL IS NOT NULL"), + Expr::UnaryOp { + op: UnaryOperator::Not, + .. + } + ); + + // for unsupported dialects, parsing should stop at `NOT NULL` + notnull_unsupported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL"); +} + +#[test] +fn parse_odbc_time_date_timestamp() { + // Supported statements + let sql_d = "SELECT {d '2025-07-17'}, category_name FROM categories"; + let _ = all_dialects().verified_stmt(sql_d); + let sql_t = "SELECT {t '14:12:01'}, category_name FROM categories"; + let _ = all_dialects().verified_stmt(sql_t); + let sql_ts = "SELECT {ts '2025-07-17 14:12:01'}, category_name FROM categories"; + let _ = all_dialects().verified_stmt(sql_ts); + // Unsupported statement + let supports_dictionary = all_dialects_where(|d| d.supports_dictionary_syntax()); + let dictionary_unsupported = all_dialects_where(|d| !d.supports_dictionary_syntax()); + let sql = "SELECT {tt '14:12:01'} FROM foo"; + let res = supports_dictionary.parse_sql_statements(sql); + let res_dict = dictionary_unsupported.parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError("Expected: :, found: '14:12:01'".to_string()), + res.unwrap_err() + ); + assert_eq!( + ParserError::ParserError("Expected: an expression, found: {".to_string()), + res_dict.unwrap_err() + ); +} + +#[test] +fn parse_create_user() { + let create = verified_stmt("CREATE USER u1"); + match create { + Statement::CreateUser(stmt) => { + assert_eq!(stmt.name, Ident::new("u1")); + } + _ => unreachable!(), + } + verified_stmt("CREATE OR REPLACE USER u1"); + verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1"); + verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret'"); + verified_stmt( + "CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=TRUE", + ); + verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=TRUE TYPE=SERVICE TAG (t1='v1')"); + let create = verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=TRUE TYPE=SERVICE WITH TAG (t1='v1', t2='v2')"); + match create { + Statement::CreateUser(stmt) => { + assert_eq!(stmt.name, Ident::new("u1")); + assert_eq!(stmt.or_replace, true); + assert_eq!(stmt.if_not_exists, true); + assert_eq!( + stmt.options, + KeyValueOptions { + delimiter: KeyValueOptionsDelimiter::Space, + options: vec![ + KeyValueOption { + option_name: "PASSWORD".to_string(), + value: "secret".to_string(), + option_type: KeyValueOptionType::STRING + }, + KeyValueOption { + option_name: "MUST_CHANGE_PASSWORD".to_string(), + value: "TRUE".to_string(), + option_type: KeyValueOptionType::BOOLEAN + }, + KeyValueOption { + option_name: "TYPE".to_string(), + value: "SERVICE".to_string(), + option_type: KeyValueOptionType::ENUM + }, + ], + }, + ); + assert_eq!(stmt.with_tags, true); + assert_eq!( + stmt.tags, + KeyValueOptions { + delimiter: KeyValueOptionsDelimiter::Comma, + options: vec![ + KeyValueOption { + option_name: "t1".to_string(), + value: "v1".to_string(), + option_type: KeyValueOptionType::STRING + }, + KeyValueOption { + option_name: "t2".to_string(), + value: "v2".to_string(), + option_type: KeyValueOptionType::STRING + }, + ] + } + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_drop_stream() { + let sql = "DROP STREAM s1"; + match verified_stmt(sql) { + Statement::Drop { + names, object_type, .. + } => { + assert_eq!( + vec!["s1"], + names.iter().map(ToString::to_string).collect::>() + ); + assert_eq!(ObjectType::Stream, object_type); + } + _ => unreachable!(), + } + verified_stmt("DROP STREAM IF EXISTS s1"); +} + +#[test] +fn parse_create_view_if_not_exists() { + // Name after IF NOT EXISTS + let sql: &'static str = "CREATE VIEW IF NOT EXISTS v AS SELECT 1"; + let _ = all_dialects().verified_stmt(sql); + // Name before IF NOT EXISTS + let sql = "CREATE VIEW v IF NOT EXISTS AS SELECT 1"; + let _ = all_dialects().verified_stmt(sql); + // Name missing from query + let sql = "CREATE VIEW IF NOT EXISTS AS SELECT 1"; + let res = all_dialects().parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError("Expected: AS, found: SELECT".to_string()), + res.unwrap_err() + ); +} + +#[test] +fn test_parse_not_null_in_column_options() { + let canonical = concat!( + "CREATE TABLE foo (", + "abc INT DEFAULT (42 IS NOT NULL) NOT NULL,", + " def INT,", + " def_null BOOL GENERATED ALWAYS AS (def IS NOT NULL) STORED,", + " CHECK (abc IS NOT NULL)", + ")" + ); + all_dialects().verified_stmt(canonical); + all_dialects().one_statement_parses_to( + concat!( + "CREATE TABLE foo (", + "abc INT DEFAULT (42 NOT NULL) NOT NULL,", + " def INT,", + " def_null BOOL GENERATED ALWAYS AS (def NOT NULL) STORED,", + " CHECK (abc NOT NULL)", + ")" + ), + canonical, + ); +} + +#[test] +fn test_parse_default_with_collate_column_option() { + let sql = "CREATE TABLE foo (abc TEXT DEFAULT 'foo' COLLATE 'en_US')"; + let stmt = all_dialects().verified_stmt(sql); + if let Statement::CreateTable(CreateTable { mut columns, .. }) = stmt { + let mut column = columns.pop().unwrap(); + assert_eq!(&column.name.value, "abc"); + assert_eq!(column.data_type, DataType::Text); + let collate_option = column.options.pop().unwrap(); + if let ColumnOptionDef { + name: None, + option: ColumnOption::Collation(collate), + } = collate_option + { + assert_eq!(collate.to_string(), "'en_US'"); + } else { + panic!("Expected collate column option, got {collate_option}"); + } + let default_option = column.options.pop().unwrap(); + if let ColumnOptionDef { + name: None, + option: ColumnOption::Default(Expr::Value(value)), + } = default_option + { + assert_eq!(value.to_string(), "'foo'"); + } else { + panic!("Expected default column option, got {default_option}"); + } + } else { + panic!("Expected create table statement"); + } +} + +#[test] +fn parse_create_table_like() { + let dialects = all_dialects_except(|d| d.supports_create_table_like_parenthesized()); + let sql = "CREATE TABLE new LIKE old"; + match dialects.verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::Plain(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: None, + })) + ) + } + _ => unreachable!(), + } + let dialects = all_dialects_where(|d| d.supports_create_table_like_parenthesized()); + let sql = "CREATE TABLE new (LIKE old)"; + match dialects.verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: None, + })) + ) + } + _ => unreachable!(), + } + let sql = "CREATE TABLE new (LIKE old INCLUDING DEFAULTS)"; + match dialects.verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: Some(CreateTableLikeDefaults::Including), + })) + ) + } + _ => unreachable!(), + } + let sql = "CREATE TABLE new (LIKE old EXCLUDING DEFAULTS)"; + match dialects.verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: Some(CreateTableLikeDefaults::Excluding), + })) + ) + } + _ => unreachable!(), + } +} + +#[test] +fn parse_copy_options() { + let copy = verified_stmt( + r#"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' IAM_ROLE 'arn:aws:iam::123456789:role/role1' CSV IGNOREHEADER 1"#, + ); + match copy { + Statement::Copy { legacy_options, .. } => { + assert_eq!( + legacy_options, + vec![ + CopyLegacyOption::IamRole(IamRoleKind::Arn( + "arn:aws:iam::123456789:role/role1".to_string() + )), + CopyLegacyOption::Csv(vec![]), + CopyLegacyOption::IgnoreHeader(1), + ] + ); + } + _ => unreachable!(), + } + + let copy = one_statement_parses_to( + r#"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' IAM_ROLE DEFAULT CSV IGNOREHEADER AS 1"#, + r#"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' IAM_ROLE DEFAULT CSV IGNOREHEADER 1"#, + ); + match copy { + Statement::Copy { legacy_options, .. } => { + assert_eq!( + legacy_options, + vec![ + CopyLegacyOption::IamRole(IamRoleKind::Default), + CopyLegacyOption::Csv(vec![]), + CopyLegacyOption::IgnoreHeader(1), + ] + ); + } + _ => unreachable!(), + } + one_statement_parses_to( + concat!( + "COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' ", + "ACCEPTANYDATE ", + "ACCEPTINVCHARS AS '*' ", + "BLANKSASNULL ", + "CSV ", + "DATEFORMAT AS 'DD-MM-YYYY' ", + "EMPTYASNULL ", + "IAM_ROLE DEFAULT ", + "IGNOREHEADER AS 1 ", + "TIMEFORMAT AS 'auto' ", + "TRUNCATECOLUMNS", + ), + concat!( + "COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' ", + "ACCEPTANYDATE ", + "ACCEPTINVCHARS '*' ", + "BLANKSASNULL ", + "CSV ", + "DATEFORMAT 'DD-MM-YYYY' ", + "EMPTYASNULL ", + "IAM_ROLE DEFAULT ", + "IGNOREHEADER 1 ", + "TIMEFORMAT 'auto' ", + "TRUNCATECOLUMNS", + ), + ); + one_statement_parses_to( + "COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' FORMAT AS CSV", + "COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' CSV", + ); +} + +#[test] +fn test_parse_semantic_view_table_factor() { + let dialects = all_dialects_where(|d| d.supports_semantic_view_table_factor()); + + let valid_sqls = [ + ("SELECT * FROM SEMANTIC_VIEW(model)", None), + ( + "SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1, dim2)", + None, + ), + ("SELECT * FROM SEMANTIC_VIEW(a.b METRICS c.d, c.e)", None), + ( + "SELECT * FROM SEMANTIC_VIEW(model FACTS fact1, fact2)", + None, + ), + ( + "SELECT * FROM SEMANTIC_VIEW(model FACTS DATE_PART('year', col))", + None, + ), + ( + "SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 METRICS met1)", + None, + ), + ( + "SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 WHERE x > 0)", + None, + ), + ( + "SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1) AS sv", + None, + ), + ( + "SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS DATE_PART('year', col))", + None, + ), + ( + "SELECT * FROM SEMANTIC_VIEW(model METRICS orders.col, orders.col2)", + None, + ), + // We can parse in any order but will always produce a result in a fixed order. + ( + "SELECT * FROM SEMANTIC_VIEW(model WHERE x > 0 DIMENSIONS dim1)", + Some("SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 WHERE x > 0)"), + ), + ( + "SELECT * FROM SEMANTIC_VIEW(model METRICS met1 DIMENSIONS dim1)", + Some("SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 METRICS met1)"), + ), + ( + "SELECT * FROM SEMANTIC_VIEW(model FACTS fact1 DIMENSIONS dim1)", + Some("SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 FACTS fact1)"), + ), + ]; + + for (input_sql, expected_sql) in valid_sqls { + if let Some(expected) = expected_sql { + // Test that non-canonical order gets normalized + let parsed = dialects.parse_sql_statements(input_sql).unwrap(); + let formatted = parsed[0].to_string(); + assert_eq!(formatted, expected); + } else { + dialects.verified_stmt(input_sql); + } + } + + let invalid_sqls = [ + "SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 INVALID inv1)", + "SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 DIMENSIONS dim2)", + "SELECT * FROM SEMANTIC_VIEW(model METRICS SUM(met1.avg))", + ]; + + for sql in invalid_sqls { + let result = dialects.parse_sql_statements(sql); + assert!(result.is_err(), "Expected error for invalid SQL: {}", sql); + } + + let ast_sql = r#"SELECT * FROM SEMANTIC_VIEW( + my_model + DIMENSIONS DATE_PART('year', date_col), region_name + METRICS orders.revenue, orders.count + WHERE active = true + ) AS model_alias"#; + + let stmt = dialects.parse_sql_statements(ast_sql).unwrap(); + match &stmt[0] { + Statement::Query(q) => { + if let SetExpr::Select(select) = q.body.as_ref() { + if let Some(TableWithJoins { relation, .. }) = select.from.first() { + match relation { + TableFactor::SemanticView { + name, + dimensions, + metrics, + facts, + where_clause, + alias, + } => { + assert_eq!(name.to_string(), "my_model"); + assert_eq!(dimensions.len(), 2); + assert_eq!(dimensions[0].to_string(), "DATE_PART('year', date_col)"); + assert_eq!(dimensions[1].to_string(), "region_name"); + assert_eq!(metrics.len(), 2); + assert_eq!(metrics[0].to_string(), "orders.revenue"); + assert_eq!(metrics[1].to_string(), "orders.count"); + assert!(facts.is_empty()); + assert!(where_clause.is_some()); + assert_eq!(where_clause.as_ref().unwrap().to_string(), "active = true"); + assert!(alias.is_some()); + assert_eq!(alias.as_ref().unwrap().name.value, "model_alias"); + } + _ => panic!("Expected SemanticView table factor"), + } + } else { + panic!("Expected table in FROM clause"); + } + } else { + panic!("Expected SELECT statement"); + } + } + _ => panic!("Expected Query statement"), + } +} diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 88aae499a..e01611b6f 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -15,9 +15,11 @@ // specific language governing permissions and limitations // under the License. +use sqlparser::ast::helpers::attached_token::AttachedToken; use sqlparser::ast::*; use sqlparser::dialect::{DatabricksDialect, GenericDialect}; use sqlparser::parser::ParserError; +use sqlparser::tokenizer::Span; use test_utils::*; #[macro_use] @@ -108,6 +110,8 @@ fn test_databricks_lambdas() { Expr::Lambda(LambdaFunction { params: OneOrManyWithParens::Many(vec![Ident::new("p1"), Ident::new("p2")]), body: Box::new(Expr::Case { + case_token: AttachedToken::empty(), + end_token: AttachedToken::empty(), operand: None, conditions: vec![ CaseWhen { @@ -210,7 +214,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - databricks().verified_stmt(&format!("USE {}", object_name)), + databricks().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -218,7 +222,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - databricks().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), + databricks().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -230,21 +234,21 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with keyword and different type of quotes assert_eq!( - databricks().verified_stmt(&format!("USE CATALOG {0}my_catalog{0}", quote)), + databricks().verified_stmt(&format!("USE CATALOG {quote}my_catalog{quote}")), Statement::Use(Use::Catalog(ObjectName::from(vec![Ident::with_quote( quote, "my_catalog".to_string(), )]))) ); assert_eq!( - databricks().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)), + databricks().verified_stmt(&format!("USE DATABASE {quote}my_database{quote}")), Statement::Use(Use::Database(ObjectName::from(vec![Ident::with_quote( quote, "my_database".to_string(), )]))) ); assert_eq!( - databricks().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)), + databricks().verified_stmt(&format!("USE SCHEMA {quote}my_schema{quote}")), Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote( quote, "my_schema".to_string(), @@ -323,10 +327,14 @@ fn data_type_timestamp_ntz() { // Literal assert_eq!( databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"), - Expr::TypedString { + Expr::TypedString(TypedString { data_type: DataType::TimestampNtz, - value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()) - } + value: ValueWithSpan { + value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) ); // Cast @@ -354,6 +362,6 @@ fn data_type_timestamp_ntz() { }] ); } - s => panic!("Unexpected statement: {:?}", s), + s => panic!("Unexpected statement: {s:?}"), } } diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 320583248..5bad73365 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -24,6 +24,7 @@ use test_utils::*; use sqlparser::ast::*; use sqlparser::dialect::{DuckDbDialect, GenericDialect}; +use sqlparser::parser::ParserError; fn duckdb() -> TestedDialects { TestedDialects::new(vec![Box::new(DuckDbDialect {})]) @@ -44,10 +45,12 @@ fn test_struct() { StructField { field_name: Some(Ident::new("v")), field_type: DataType::Varchar(None), + options: None, }, StructField { field_name: Some(Ident::new("i")), field_type: DataType::Integer(None), + options: None, }, ], StructBracketKind::Parentheses, @@ -84,6 +87,7 @@ fn test_struct() { StructField { field_name: Some(Ident::new("v")), field_type: DataType::Varchar(None), + options: None, }, StructField { field_name: Some(Ident::new("s")), @@ -92,14 +96,17 @@ fn test_struct() { StructField { field_name: Some(Ident::new("a1")), field_type: DataType::Integer(None), + options: None, }, StructField { field_name: Some(Ident::new("a2")), field_type: DataType::Varchar(None), + options: None, }, ], StructBracketKind::Parentheses, ), + options: None, }, ], StructBracketKind::Parentheses, @@ -262,6 +269,7 @@ fn test_select_union_by_name() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { @@ -292,6 +300,7 @@ fn test_select_union_by_name() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { @@ -362,7 +371,7 @@ fn test_duckdb_specific_int_types() { ("HUGEINT", DataType::HugeInt), ]; for (dtype_string, data_type) in duckdb_dtypes { - let sql = format!("SELECT 123::{}", dtype_string); + let sql = format!("SELECT 123::{dtype_string}"); let select = duckdb().verified_only_select(&sql); assert_eq!( &Expr::Cast { @@ -691,6 +700,7 @@ fn test_duckdb_union_datatype() { transient: Default::default(), volatile: Default::default(), iceberg: Default::default(), + dynamic: Default::default(), name: ObjectName::from(vec!["tbl1".into()]), columns: vec![ ColumnDef { @@ -735,19 +745,13 @@ fn test_duckdb_union_datatype() { storage: Default::default(), location: Default::default() }), - table_properties: Default::default(), - with_options: Default::default(), file_format: Default::default(), location: Default::default(), query: Default::default(), without_rowid: Default::default(), like: Default::default(), clone: Default::default(), - engine: Default::default(), comment: Default::default(), - auto_increment_offset: Default::default(), - default_charset: Default::default(), - collation: Default::default(), on_commit: Default::default(), on_cluster: Default::default(), primary_key: Default::default(), @@ -755,7 +759,6 @@ fn test_duckdb_union_datatype() { partition_by: Default::default(), cluster_by: Default::default(), clustered_by: Default::default(), - options: Default::default(), inherits: Default::default(), strict: Default::default(), copy_grants: Default::default(), @@ -772,6 +775,13 @@ fn test_duckdb_union_datatype() { catalog: Default::default(), catalog_sync: Default::default(), storage_serialization_policy: Default::default(), + table_options: CreateTableOptions::None, + target_lag: None, + warehouse: None, + version: None, + refresh_mode: None, + initialize: None, + require_user: Default::default(), }), stmt ); @@ -792,7 +802,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {}", object_name)), + duckdb().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -800,7 +810,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), + duckdb().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -812,7 +822,9 @@ fn parse_use() { for "e in "e_styles { // Test double identifier with different type of quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), + duckdb().verified_stmt(&format!( + "USE {quote}CATALOG{quote}.{quote}my_schema{quote}" + )), Statement::Use(Use::Object(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") @@ -828,3 +840,32 @@ fn parse_use() { ]))) ); } + +#[test] +fn test_duckdb_trim() { + let real_sql = r#"SELECT customer_id, TRIM(item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; + assert_eq!(duckdb().verified_stmt(real_sql).to_string(), real_sql); + + let sql_only_select = "SELECT TRIM('xyz', 'a')"; + let select = duckdb().verified_only_select(sql_only_select); + assert_eq!( + &Expr::Trim { + expr: Box::new(Expr::Value( + Value::SingleQuotedString("xyz".to_owned()).with_empty_span() + )), + trim_where: None, + trim_what: None, + trim_characters: Some(vec![Expr::Value( + Value::SingleQuotedString("a".to_owned()).with_empty_span() + )]), + }, + expr_from_projection(only(&select.projection)) + ); + + // missing comma separation + let error_sql = "SELECT TRIM('xyz' 'a')"; + assert_eq!( + ParserError::ParserError("Expected: ), found: 'a'".to_owned()), + duckdb().parse_sql_statements(error_sql).unwrap_err() + ); +} diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 9b0430947..56a72ec84 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -133,9 +133,7 @@ fn create_table_with_comment() { Statement::CreateTable(CreateTable { comment, .. }) => { assert_eq!( comment, - Some(CommentDef::AfterColumnDefsWithoutEq( - "table comment".to_string() - )) + Some(CommentDef::WithoutEq("table comment".to_string())) ) } _ => unreachable!(), @@ -343,6 +341,9 @@ fn lateral_view() { fn sort_by() { let sort_by = "SELECT * FROM db.table SORT BY a"; hive().verified_stmt(sort_by); + + let sort_by_with_direction = "SELECT * FROM db.table SORT BY a, b DESC"; + hive().verified_stmt(sort_by_with_direction); } #[test] @@ -523,7 +524,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - hive().verified_stmt(&format!("USE {}", object_name)), + hive().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -531,7 +532,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - hive().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + hive().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ef6103474..a1e05d030 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -23,7 +23,8 @@ mod test_utils; use helpers::attached_token::AttachedToken; -use sqlparser::tokenizer::{Location, Span}; +use sqlparser::keywords::Keyword; +use sqlparser::tokenizer::{Location, Span, Token, TokenWithSpan, Word}; use test_utils::*; use sqlparser::ast::DataType::{Int, Text, Varbinary}; @@ -31,7 +32,7 @@ use sqlparser::ast::DeclareAssignment::MsSqlAssignment; use sqlparser::ast::Value::SingleQuotedString; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MsSqlDialect}; -use sqlparser::parser::{Parser, ParserError}; +use sqlparser::parser::{Parser, ParserError, ParserOptions}; #[test] fn parse_mssql_identifiers() { @@ -99,47 +100,53 @@ fn parse_mssql_delimited_identifiers() { #[test] fn parse_create_procedure() { - let sql = "CREATE OR ALTER PROCEDURE test (@foo INT, @bar VARCHAR(256)) AS BEGIN SELECT 1 END"; + let sql = "CREATE OR ALTER PROCEDURE test (@foo INT, @bar VARCHAR(256)) AS BEGIN SELECT 1; END"; assert_eq!( ms().verified_stmt(sql), Statement::CreateProcedure { or_alter: true, - body: vec![Statement::Query(Box::new(Query { - with: None, - limit_clause: None, - fetch: None, - locks: vec![], - for_clause: None, - order_by: None, - settings: None, - format_clause: None, - body: Box::new(SetExpr::Select(Box::new(Select { - select_token: AttachedToken::empty(), - distinct: None, - top: None, - top_before_distinct: false, - projection: vec![SelectItem::UnnamedExpr(Expr::Value( - (number("1")).with_empty_span() - ))], - into: None, - from: vec![], - lateral_views: vec![], - prewhere: None, - selection: None, - group_by: GroupByExpr::Expressions(vec![], vec![]), - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None, - named_window: vec![], - window_before_qualify: false, - qualify: None, - value_table_mode: None, - connect_by: None, - flavor: SelectFlavor::Standard, - }))) - }))], + body: ConditionalStatements::BeginEnd(BeginEndStatements { + begin_token: AttachedToken::empty(), + statements: vec![Statement::Query(Box::new(Query { + with: None, + limit_clause: None, + fetch: None, + locks: vec![], + for_clause: None, + order_by: None, + settings: None, + format_clause: None, + pipe_operators: vec![], + body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![SelectItem::UnnamedExpr(Expr::Value( + (number("1")).with_empty_span() + ))], + exclude: None, + into: None, + from: vec![], + lateral_views: vec![], + prewhere: None, + selection: None, + group_by: GroupByExpr::Expressions(vec![], vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + window_before_qualify: false, + qualify: None, + value_table_mode: None, + connect_by: None, + flavor: SelectFlavor::Standard, + }))) + }))], + end_token: AttachedToken::empty(), + }), params: Some(vec![ ProcedureParam { name: Ident { @@ -147,7 +154,8 @@ fn parse_create_procedure() { quote_style: None, span: Span::empty(), }, - data_type: DataType::Int(None) + data_type: DataType::Int(None), + mode: None, }, ProcedureParam { name: Ident { @@ -158,33 +166,36 @@ fn parse_create_procedure() { data_type: DataType::Varchar(Some(CharacterLength::IntegerLength { length: 256, unit: None - })) + })), + mode: None, } ]), name: ObjectName::from(vec![Ident { value: "test".into(), quote_style: None, span: Span::empty(), - }]) + }]), + language: None, } ) } #[test] fn parse_mssql_create_procedure() { - let _ = ms_and_generic().verified_stmt("CREATE OR ALTER PROCEDURE foo AS BEGIN SELECT 1 END"); - let _ = ms_and_generic().verified_stmt("CREATE PROCEDURE foo AS BEGIN SELECT 1 END"); + let _ = ms_and_generic().verified_stmt("CREATE OR ALTER PROCEDURE foo AS SELECT 1;"); + let _ = ms_and_generic().verified_stmt("CREATE OR ALTER PROCEDURE foo AS BEGIN SELECT 1; END"); + let _ = ms_and_generic().verified_stmt("CREATE PROCEDURE foo AS BEGIN SELECT 1; END"); let _ = ms().verified_stmt( - "CREATE PROCEDURE foo AS BEGIN SELECT [myColumn] FROM [myschema].[mytable] END", + "CREATE PROCEDURE foo AS BEGIN SELECT [myColumn] FROM [myschema].[mytable]; END", ); let _ = ms_and_generic().verified_stmt( - "CREATE PROCEDURE foo (@CustomerName NVARCHAR(50)) AS BEGIN SELECT * FROM DEV END", + "CREATE PROCEDURE foo (@CustomerName NVARCHAR(50)) AS BEGIN SELECT * FROM DEV; END", ); - let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test' END"); + let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test'; END"); // Test a statement with END in it - let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN SELECT [foo], CASE WHEN [foo] IS NULL THEN 'empty' ELSE 'notempty' END AS [foo] END"); + let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN SELECT [foo], CASE WHEN [foo] IS NULL THEN 'empty' ELSE 'notempty' END AS [foo]; END"); // Multiple statements - let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test'; SELECT [foo] FROM BAR WHERE [FOO] > 10 END"); + let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test'; SELECT [foo] FROM BAR WHERE [FOO] > 10; END"); } #[test] @@ -222,7 +233,7 @@ fn parse_create_function() { value: Some(ReturnStatementValue::Expr(Expr::Value( (number("1")).with_empty_span() ))), - }),], + })], end_token: AttachedToken::empty(), })), behavior: None, @@ -247,6 +258,12 @@ fn parse_create_function() { "; let _ = ms().verified_stmt(multi_statement_function); + let multi_statement_function_without_as = multi_statement_function.replace(" AS", ""); + let _ = ms().one_statement_parses_to( + &multi_statement_function_without_as, + multi_statement_function, + ); + let create_function_with_conditional = "\ CREATE FUNCTION some_scalar_udf() \ RETURNS INT \ @@ -271,6 +288,137 @@ fn parse_create_function() { END\ "; let _ = ms().verified_stmt(create_or_alter_function); + + let create_function_with_return_expression = "\ + CREATE FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) \ + RETURNS INT \ + AS \ + BEGIN \ + RETURN CONVERT(INT, 1) + 2; \ + END\ + "; + let _ = ms().verified_stmt(create_function_with_return_expression); + + let create_inline_table_value_function = "\ + CREATE FUNCTION some_inline_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS TABLE \ + AS \ + RETURN (SELECT 1 AS col_1)\ + "; + let _ = ms().verified_stmt(create_inline_table_value_function); + + let create_inline_table_value_function_without_parentheses = "\ + CREATE FUNCTION some_inline_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS TABLE \ + AS \ + RETURN SELECT 1 AS col_1\ + "; + let _ = ms().verified_stmt(create_inline_table_value_function_without_parentheses); + + let create_inline_table_value_function_without_as = + create_inline_table_value_function.replace(" AS", ""); + let _ = ms().one_statement_parses_to( + &create_inline_table_value_function_without_as, + create_inline_table_value_function, + ); + + let create_multi_statement_table_value_function = "\ + CREATE FUNCTION some_multi_statement_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS @t TABLE (col_1 INT) \ + AS \ + BEGIN \ + INSERT INTO @t SELECT 1; \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_multi_statement_table_value_function); + + let create_multi_statement_table_value_function_without_as = + create_multi_statement_table_value_function.replace(" AS", ""); + let _ = ms().one_statement_parses_to( + &create_multi_statement_table_value_function_without_as, + create_multi_statement_table_value_function, + ); + + let create_multi_statement_table_value_function_with_constraints = "\ + CREATE FUNCTION some_multi_statement_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS @t TABLE (col_1 INT NOT NULL) \ + AS \ + BEGIN \ + INSERT INTO @t SELECT 1; \ + RETURN @t; \ + END\ + "; + let _ = ms().verified_stmt(create_multi_statement_table_value_function_with_constraints); + + let create_multi_statement_tvf_without_table_definition = "\ + CREATE FUNCTION incorrect_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS @t TABLE () + AS \ + BEGIN \ + INSERT INTO @t SELECT 1; \ + RETURN @t; \ + END\ + "; + assert_eq!( + ParserError::ParserError("Unparsable function body".to_owned()), + ms().parse_sql_statements(create_multi_statement_tvf_without_table_definition) + .unwrap_err() + ); + + let create_inline_tvf_without_subquery_or_bare_select = "\ + CREATE FUNCTION incorrect_tvf(@foo INT, @bar VARCHAR(256)) \ + RETURNS TABLE + AS \ + RETURN 'hi'\ + "; + assert_eq!( + ParserError::ParserError( + "Expected a subquery (or bare SELECT statement) after RETURN".to_owned() + ), + ms().parse_sql_statements(create_inline_tvf_without_subquery_or_bare_select) + .unwrap_err() + ); +} + +#[test] +fn parse_create_function_parameter_default_values() { + let single_default_sql = + "CREATE FUNCTION test_func(@param1 INT = 42) RETURNS INT AS BEGIN RETURN @param1; END"; + assert_eq!( + ms().verified_stmt(single_default_sql), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: false, + temporary: false, + if_not_exists: false, + name: ObjectName::from(vec![Ident::new("test_func")]), + args: Some(vec![OperateFunctionArg { + mode: None, + name: Some(Ident::new("@param1")), + data_type: DataType::Int(None), + default_expr: Some(Expr::Value((number("42")).with_empty_span())), + },]), + return_type: Some(DataType::Int(None)), + function_body: Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { + begin_token: AttachedToken::empty(), + statements: vec![Statement::Return(ReturnStatement { + value: Some(ReturnStatementValue::Expr(Expr::Identifier(Ident::new( + "@param1" + )))), + })], + end_token: AttachedToken::empty(), + })), + behavior: None, + called_on_null: None, + parallel: None, + using: None, + language: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }), + ); } #[test] @@ -1221,6 +1369,7 @@ fn parse_substring_in_select() { special: true, shorthand: false, })], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident { @@ -1252,6 +1401,7 @@ fn parse_substring_in_select() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), query ); @@ -1354,6 +1504,8 @@ fn parse_mssql_declare() { order_by: None, settings: None, format_clause: None, + pipe_operators: vec![], + body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, @@ -1366,6 +1518,7 @@ fn parse_mssql_declare() { (Value::Number("4".parse().unwrap(), false)).with_empty_span() )), })], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1393,6 +1546,85 @@ fn parse_mssql_declare() { let _ = ms().verified_stmt(declare_cursor_for_select); } +#[test] +fn test_mssql_cursor() { + let full_cursor_usage = "\ + DECLARE Employee_Cursor CURSOR FOR \ + SELECT LastName, FirstName \ + FROM AdventureWorks2022.HumanResources.vEmployee \ + WHERE LastName LIKE 'B%'; \ + \ + OPEN Employee_Cursor; \ + \ + FETCH NEXT FROM Employee_Cursor; \ + \ + WHILE @@FETCH_STATUS = 0 \ + BEGIN \ + FETCH NEXT FROM Employee_Cursor; \ + END; \ + \ + CLOSE Employee_Cursor; \ + DEALLOCATE Employee_Cursor\ + "; + let _ = ms().statements_parse_to(full_cursor_usage, ""); +} + +#[test] +fn test_mssql_while_statement() { + let while_single_statement = "WHILE 1 = 0 PRINT 'Hello World';"; + let stmt = ms().verified_stmt(while_single_statement); + assert_eq!( + stmt, + Statement::While(sqlparser::ast::WhileStatement { + while_block: ConditionalStatementBlock { + start_token: AttachedToken(TokenWithSpan { + token: Token::Word(Word { + value: "WHILE".to_string(), + quote_style: None, + keyword: Keyword::WHILE + }), + span: Span::empty() + }), + condition: Some(Expr::BinaryOp { + left: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value( + (Value::Number("0".parse().unwrap(), false)).with_empty_span() + )), + }), + then_token: None, + conditional_statements: ConditionalStatements::Sequence { + statements: vec![Statement::Print(PrintStatement { + message: Box::new(Expr::Value( + (Value::SingleQuotedString("Hello World".to_string())) + .with_empty_span() + )), + })], + } + } + }) + ); + + let while_begin_end = "\ + WHILE @@FETCH_STATUS = 0 \ + BEGIN \ + FETCH NEXT FROM Employee_Cursor; \ + END\ + "; + let _ = ms().verified_stmt(while_begin_end); + + let while_begin_end_multiple_statements = "\ + WHILE @@FETCH_STATUS = 0 \ + BEGIN \ + FETCH NEXT FROM Employee_Cursor; \ + PRINT 'Hello World'; \ + END\ + "; + let _ = ms().verified_stmt(while_begin_end_multiple_statements); +} + #[test] fn test_parse_raiserror() { let sql = r#"RAISERROR('This is a test', 16, 1)"#; @@ -1444,7 +1676,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - ms().verified_stmt(&format!("USE {}", object_name)), + ms().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -1452,7 +1684,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - ms().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + ms().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -1616,6 +1848,7 @@ fn parse_create_table_with_valid_options() { temporary: false, external: false, global: None, + dynamic: false, if_not_exists: false, transient: false, volatile: false, @@ -1641,7 +1874,6 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - options: vec![], }, ColumnDef { @@ -1651,7 +1883,6 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - options: vec![], }, ], @@ -1663,19 +1894,13 @@ fn parse_create_table_with_valid_options() { storage: None, location: None, },), - table_properties: vec![], - with_options, file_format: None, location: None, query: None, without_rowid: false, like: None, clone: None, - engine: None, comment: None, - auto_increment_offset: None, - default_charset: None, - collation: None, on_commit: None, on_cluster: None, primary_key: None, @@ -1683,7 +1908,6 @@ fn parse_create_table_with_valid_options() { partition_by: None, cluster_by: None, clustered_by: None, - options: None, inherits: None, strict: false, iceberg: false, @@ -1701,6 +1925,13 @@ fn parse_create_table_with_valid_options() { catalog: None, catalog_sync: None, storage_serialization_policy: None, + table_options: CreateTableOptions::With(with_options), + target_lag: None, + warehouse: None, + version: None, + refresh_mode: None, + initialize: None, + require_user: false, }) ); } @@ -1807,6 +2038,7 @@ fn parse_create_table_with_identity_column() { temporary: false, external: false, global: None, + dynamic: false, if_not_exists: false, transient: false, volatile: false, @@ -1834,19 +2066,13 @@ fn parse_create_table_with_identity_column() { storage: None, location: None, },), - table_properties: vec![], - with_options: vec![], file_format: None, location: None, query: None, without_rowid: false, like: None, clone: None, - engine: None, comment: None, - auto_increment_offset: None, - default_charset: None, - collation: None, on_commit: None, on_cluster: None, primary_key: None, @@ -1854,7 +2080,6 @@ fn parse_create_table_with_identity_column() { partition_by: None, cluster_by: None, clustered_by: None, - options: None, inherits: None, strict: false, copy_grants: false, @@ -1871,6 +2096,13 @@ fn parse_create_table_with_identity_column() { catalog: None, catalog_sync: None, storage_serialization_policy: None, + table_options: CreateTableOptions::None, + target_lag: None, + warehouse: None, + version: None, + refresh_mode: None, + initialize: None, + require_user: false, }), ); } @@ -1972,7 +2204,7 @@ fn parse_mssql_if_else() { "IF 1 = 1 BEGIN SET @A = 1; END ELSE SET @A = 2;" ); } - _ => panic!("Unexpected statements: {:?}", stmts), + _ => panic!("Unexpected statements: {stmts:?}"), } } @@ -2022,7 +2254,7 @@ fn test_mssql_if_statements_span() { Span::new(Location::new(1, 21), Location::new(1, 36)) ); } - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Blocks @@ -2043,7 +2275,7 @@ fn test_mssql_if_statements_span() { Span::new(Location::new(1, 32), Location::new(1, 57)) ); } - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } } @@ -2109,6 +2341,18 @@ fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } +// MS SQL dialect with support for optional semi-colon statement delimiters +fn tsql() -> TestedDialects { + TestedDialects::new_with_options( + vec![Box::new(MsSqlDialect {})], + ParserOptions { + trailing_commas: false, + unescape: true, + require_semicolon_stmt_delimiter: false, + }, + ) +} + fn ms_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) } @@ -2129,6 +2373,103 @@ fn parse_mssql_merge_with_output() { ms_and_generic().verified_stmt(stmt); } +#[test] +fn parse_create_trigger() { + let create_trigger = "\ + CREATE OR ALTER TRIGGER reminder1 \ + ON Sales.Customer \ + AFTER INSERT, UPDATE \ + AS RAISERROR('Notify Customer Relations', 16, 10);\ + "; + let create_stmt = ms().verified_stmt(create_trigger); + assert_eq!( + create_stmt, + Statement::CreateTrigger { + or_alter: true, + or_replace: false, + is_constraint: false, + name: ObjectName::from(vec![Ident::new("reminder1")]), + period: TriggerPeriod::After, + period_before_table: false, + events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![]),], + table_name: ObjectName::from(vec![Ident::new("Sales"), Ident::new("Customer")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: TriggerObject::Statement, + include_each: false, + condition: None, + exec_body: None, + statements_as: true, + statements: Some(ConditionalStatements::Sequence { + statements: vec![Statement::RaisError { + message: Box::new(Expr::Value( + (Value::SingleQuotedString("Notify Customer Relations".to_string())) + .with_empty_span() + )), + severity: Box::new(Expr::Value( + (Value::Number("16".parse().unwrap(), false)).with_empty_span() + )), + state: Box::new(Expr::Value( + (Value::Number("10".parse().unwrap(), false)).with_empty_span() + )), + arguments: vec![], + options: vec![], + }], + }), + characteristics: None, + } + ); + + let multi_statement_as_trigger = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + DECLARE @var INT; \ + RAISERROR('Trigger fired', 10, 1);\ + "; + let _ = ms().verified_stmt(multi_statement_as_trigger); + + let multi_statement_trigger = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + DECLARE @var INT; \ + RAISERROR('Trigger fired', 10, 1); \ + END\ + "; + let _ = ms().verified_stmt(multi_statement_trigger); + + let create_trigger_with_return = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_trigger_with_return); + + let create_trigger_with_return = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_trigger_with_return); + + let create_trigger_with_conditional = "\ + CREATE TRIGGER some_trigger ON some_table FOR INSERT \ + AS \ + BEGIN \ + IF 1 = 2 \ + BEGIN \ + RAISERROR('Trigger fired', 10, 1); \ + END; \ + RETURN; \ + END\ + "; + let _ = ms().verified_stmt(create_trigger_with_conditional); +} + #[test] fn parse_drop_trigger() { let sql_drop_trigger = "DROP TRIGGER emp_stamp;"; @@ -2160,3 +2501,25 @@ fn parse_print() { let _ = ms().verified_stmt("PRINT N'Hello, ⛄️!'"); let _ = ms().verified_stmt("PRINT @my_variable"); } + +#[test] +fn parse_mssql_grant() { + ms().verified_stmt("GRANT SELECT ON my_table TO public, db_admin"); +} + +#[test] +fn parse_mssql_deny() { + ms().verified_stmt("DENY SELECT ON my_table TO public, db_admin"); +} + +#[test] +fn test_tsql_no_semicolon_delimiter() { + let sql = r#" +DECLARE @X AS NVARCHAR(MAX)='x' +DECLARE @Y AS NVARCHAR(MAX)='y' + "#; + + let stmts = tsql().parse_sql_statements(sql).unwrap(); + assert_eq!(stmts.len(), 2); + assert!(stmts.iter().all(|s| matches!(s, Statement::Declare { .. }))); +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f74248b86..184532035 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -593,7 +593,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - mysql_and_generic().verified_stmt(&format!("USE {}", object_name)), + mysql_and_generic().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -601,8 +601,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - mysql_and_generic() - .verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + mysql_and_generic().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -670,6 +669,20 @@ fn table_constraint_unique_primary_ctor( characteristics: Option, unique_index_type_display: Option, ) -> TableConstraint { + let columns = columns + .into_iter() + .map(|ident| IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(ident), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: None, + }) + .collect(); match unique_index_type_display { Some(index_type_display) => TableConstraint::Unique { name, @@ -795,6 +808,67 @@ fn parse_create_table_primary_and_unique_key_with_index_options() { } } +#[test] +fn parse_prefix_key_part() { + let expected = vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::value( + number("10"), + )))]; + for sql in [ + "CREATE INDEX idx_index ON t(textcol(10))", + "ALTER TABLE tab ADD INDEX idx_index (textcol(10))", + "ALTER TABLE tab ADD PRIMARY KEY (textcol(10))", + "ALTER TABLE tab ADD UNIQUE KEY (textcol(10))", + "ALTER TABLE tab ADD UNIQUE KEY (textcol(10))", + "ALTER TABLE tab ADD FULLTEXT INDEX (textcol(10))", + "CREATE TABLE t (textcol TEXT, INDEX idx_index (textcol(10)))", + ] { + match index_column(mysql_and_generic().verified_stmt(sql)) { + Expr::Function(Function { + name, + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => { + assert_eq!(name.to_string(), "textcol"); + assert_eq!(args, expected); + } + expr => panic!("unexpected expression {expr} for {sql}"), + } + } +} + +#[test] +fn test_functional_key_part() { + assert_eq!( + index_column( + mysql_and_generic() + .verified_stmt("CREATE INDEX idx_index ON t((col COLLATE utf8mb4_bin) DESC)") + ), + Expr::Nested(Box::new(Expr::Collate { + expr: Box::new(Expr::Identifier("col".into())), + collation: ObjectName(vec![sqlparser::ast::ObjectNamePart::Identifier( + Ident::new("utf8mb4_bin") + )]), + })) + ); + assert_eq!( + index_column(mysql_and_generic().verified_stmt( + r#"CREATE TABLE t (jsoncol JSON, PRIMARY KEY ((CAST(col ->> '$.id' AS UNSIGNED)) ASC))"# + )), + Expr::Nested(Box::new(Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col"))), + op: BinaryOperator::LongArrow, + right: Box::new(Expr::Value( + Value::SingleQuotedString("$.id".to_string()).with_empty_span() + )), + }), + data_type: DataType::Unsigned, + format: None, + })), + ); +} + #[test] fn parse_create_table_primary_and_unique_key_with_index_type() { let sqls = ["UNIQUE", "PRIMARY KEY"].map(|key_ty| { @@ -848,9 +922,23 @@ fn parse_create_table_comment() { for sql in [without_equal, with_equal] { match mysql().verified_stmt(sql) { - Statement::CreateTable(CreateTable { name, comment, .. }) => { + Statement::CreateTable(CreateTable { + name, + table_options, + .. + }) => { assert_eq!(name.to_string(), "foo"); - assert_eq!(comment.expect("Should exist").to_string(), "baz"); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + let comment = match plain_options.first().unwrap() { + SqlOption::Comment(CommentDef::WithEq(c)) + | SqlOption::Comment(CommentDef::WithoutEq(c)) => c, + _ => unreachable!(), + }; + assert_eq!(comment, "baz"); } _ => unreachable!(), } @@ -859,29 +947,226 @@ fn parse_create_table_comment() { #[test] fn parse_create_table_auto_increment_offset() { - let canonical = - "CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT 123"; - let with_equal = - "CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=123"; + let sql = + "CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) ENGINE = InnoDB AUTO_INCREMENT = 123"; + + match mysql().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + table_options, + .. + }) => { + assert_eq!(name.to_string(), "foo"); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; - for sql in [canonical, with_equal] { - match mysql().one_statement_parses_to(sql, canonical) { + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("AUTO_INCREMENT"), + value: Expr::Value(test_utils::number("123").with_empty_span()) + })); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_table_multiple_options_order_independent() { + let sql1 = "CREATE TABLE mytable (id INT) ENGINE=InnoDB ROW_FORMAT=DYNAMIC KEY_BLOCK_SIZE=8 COMMENT='abc'"; + let sql2 = "CREATE TABLE mytable (id INT) KEY_BLOCK_SIZE=8 COMMENT='abc' ENGINE=InnoDB ROW_FORMAT=DYNAMIC"; + let sql3 = "CREATE TABLE mytable (id INT) ROW_FORMAT=DYNAMIC KEY_BLOCK_SIZE=8 COMMENT='abc' ENGINE=InnoDB"; + + for sql in [sql1, sql2, sql3] { + match mysql().parse_sql_statements(sql).unwrap().pop().unwrap() { Statement::CreateTable(CreateTable { name, - auto_increment_offset, + table_options, .. }) => { - assert_eq!(name.to_string(), "foo"); - assert_eq!( - auto_increment_offset.expect("Should exist").to_string(), - "123" - ); + assert_eq!(name.to_string(), "mytable"); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new("InnoDB")), + values: vec![] + } + ))); + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("KEY_BLOCK_SIZE"), + value: Expr::Value(test_utils::number("8").with_empty_span()) + })); + + assert!(plain_options + .contains(&SqlOption::Comment(CommentDef::WithEq("abc".to_owned())))); + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("ROW_FORMAT"), + value: Expr::Identifier(Ident::new("DYNAMIC".to_owned())) + })); } _ => unreachable!(), } } } +#[test] +fn parse_create_table_with_all_table_options() { + let sql = + "CREATE TABLE foo (bar INT NOT NULL AUTO_INCREMENT) ENGINE = InnoDB AUTO_INCREMENT = 123 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci INSERT_METHOD = FIRST KEY_BLOCK_SIZE = 8 ROW_FORMAT = DYNAMIC DATA DIRECTORY = '/var/lib/mysql/data' INDEX DIRECTORY = '/var/lib/mysql/index' PACK_KEYS = 1 STATS_AUTO_RECALC = 1 STATS_PERSISTENT = 0 STATS_SAMPLE_PAGES = 128 DELAY_KEY_WRITE = 1 COMPRESSION = 'ZLIB' ENCRYPTION = 'Y' MAX_ROWS = 10000 MIN_ROWS = 10 AUTOEXTEND_SIZE = 64 AVG_ROW_LENGTH = 128 CHECKSUM = 1 CONNECTION = 'mysql://localhost' ENGINE_ATTRIBUTE = 'primary' PASSWORD = 'secure_password' SECONDARY_ENGINE_ATTRIBUTE = 'secondary_attr' START TRANSACTION TABLESPACE my_tablespace STORAGE DISK UNION = (table1, table2, table3)"; + + match mysql().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + table_options, + .. + }) => { + assert_eq!(name, vec![Ident::new("foo".to_owned())].into()); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new("InnoDB")), + values: vec![] + } + ))); + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("COLLATE"), + value: Expr::Identifier(Ident::new("utf8mb4_0900_ai_ci".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("DEFAULT CHARSET"), + value: Expr::Identifier(Ident::new("utf8mb4".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("AUTO_INCREMENT"), + value: Expr::value(test_utils::number("123")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("KEY_BLOCK_SIZE"), + value: Expr::value(test_utils::number("8")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("ROW_FORMAT"), + value: Expr::Identifier(Ident::new("DYNAMIC".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("PACK_KEYS"), + value: Expr::value(test_utils::number("1")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("STATS_AUTO_RECALC"), + value: Expr::value(test_utils::number("1")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("STATS_PERSISTENT"), + value: Expr::value(test_utils::number("0")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("STATS_SAMPLE_PAGES"), + value: Expr::value(test_utils::number("128")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("STATS_SAMPLE_PAGES"), + value: Expr::value(test_utils::number("128")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("INSERT_METHOD"), + value: Expr::Identifier(Ident::new("FIRST".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("COMPRESSION"), + value: Expr::value(Value::SingleQuotedString("ZLIB".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("ENCRYPTION"), + value: Expr::value(Value::SingleQuotedString("Y".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("MAX_ROWS"), + value: Expr::value(test_utils::number("10000")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("MIN_ROWS"), + value: Expr::value(test_utils::number("10")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("AUTOEXTEND_SIZE"), + value: Expr::value(test_utils::number("64")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("AVG_ROW_LENGTH"), + value: Expr::value(test_utils::number("128")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("CHECKSUM"), + value: Expr::value(test_utils::number("1")) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("CONNECTION"), + value: Expr::value(Value::SingleQuotedString("mysql://localhost".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("ENGINE_ATTRIBUTE"), + value: Expr::value(Value::SingleQuotedString("primary".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("PASSWORD"), + value: Expr::value(Value::SingleQuotedString("secure_password".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("SECONDARY_ENGINE_ATTRIBUTE"), + value: Expr::value(Value::SingleQuotedString("secondary_attr".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::Ident(Ident::new( + "START TRANSACTION".to_owned() + )))); + assert!( + plain_options.contains(&SqlOption::TableSpace(TablespaceOption { + name: "my_tablespace".to_string(), + storage: Some(StorageType::Disk), + })) + ); + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("UNION"), + name: None, + values: vec![ + Ident::new("table1".to_string()), + Ident::new("table2".to_string()), + Ident::new("table3".to_string()) + ] + } + ))); + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("DATA DIRECTORY"), + value: Expr::value(Value::SingleQuotedString("/var/lib/mysql/data".to_owned())) + })); + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("INDEX DIRECTORY"), + value: Expr::value(Value::SingleQuotedString("/var/lib/mysql/index".to_owned())) + })); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_table_set_enum() { let sql = "CREATE TABLE foo (bar SET('a', 'b'), baz ENUM('a', 'b'))"; @@ -916,13 +1201,12 @@ fn parse_create_table_set_enum() { #[test] fn parse_create_table_engine_default_charset() { - let sql = "CREATE TABLE foo (id INT(11)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"; + let sql = "CREATE TABLE foo (id INT(11)) ENGINE = InnoDB DEFAULT CHARSET = utf8mb3"; match mysql().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, columns, - engine, - default_charset, + table_options, .. }) => { assert_eq!(name.to_string(), "foo"); @@ -934,14 +1218,24 @@ fn parse_create_table_engine_default_charset() { },], columns ); - assert_eq!( - engine, - Some(TableEngine { - name: "InnoDB".to_string(), - parameters: None - }) - ); - assert_eq!(default_charset, Some("utf8mb3".to_string())); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("DEFAULT CHARSET"), + value: Expr::Identifier(Ident::new("utf8mb3".to_owned())) + })); + + assert!(plain_options.contains(&SqlOption::NamedParenthesizedList( + NamedParenthesizedList { + key: Ident::new("ENGINE"), + name: Some(Ident::new("InnoDB")), + values: vec![] + } + ))); } _ => unreachable!(), } @@ -949,12 +1243,12 @@ fn parse_create_table_engine_default_charset() { #[test] fn parse_create_table_collate() { - let sql = "CREATE TABLE foo (id INT(11)) COLLATE=utf8mb4_0900_ai_ci"; + let sql = "CREATE TABLE foo (id INT(11)) COLLATE = utf8mb4_0900_ai_ci"; match mysql().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, columns, - collation, + table_options, .. }) => { assert_eq!(name.to_string(), "foo"); @@ -966,7 +1260,16 @@ fn parse_create_table_collate() { },], columns ); - assert_eq!(collation, Some("utf8mb4_0900_ai_ci".to_string())); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("COLLATE"), + value: Expr::Identifier(Ident::new("utf8mb4_0900_ai_ci".to_owned())) + })); } _ => unreachable!(), } @@ -974,16 +1277,26 @@ fn parse_create_table_collate() { #[test] fn parse_create_table_both_options_and_as_query() { - let sql = "CREATE TABLE foo (id INT(11)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb4_0900_ai_ci AS SELECT 1"; + let sql = "CREATE TABLE foo (id INT(11)) ENGINE = InnoDB DEFAULT CHARSET = utf8mb3 COLLATE = utf8mb4_0900_ai_ci AS SELECT 1"; match mysql_and_generic().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, - collation, query, + table_options, .. }) => { assert_eq!(name.to_string(), "foo"); - assert_eq!(collation, Some("utf8mb4_0900_ai_ci".to_string())); + + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + assert!(plain_options.contains(&SqlOption::KeyValue { + key: Ident::new("COLLATE"), + value: Expr::Identifier(Ident::new("utf8mb4_0900_ai_ci".to_owned())) + })); + assert_eq!( query.unwrap().body.as_select().unwrap().projection, vec![SelectItem::UnnamedExpr(Expr::Value( @@ -994,7 +1307,8 @@ fn parse_create_table_both_options_and_as_query() { _ => unreachable!(), } - let sql = r"CREATE TABLE foo (id INT(11)) ENGINE=InnoDB AS SELECT 1 DEFAULT CHARSET=utf8mb3"; + let sql = + r"CREATE TABLE foo (id INT(11)) ENGINE = InnoDB AS SELECT 1 DEFAULT CHARSET = utf8mb3"; assert!(matches!( mysql_and_generic().parse_sql_statements(sql), Err(ParserError::ParserError(_)) @@ -1047,6 +1361,13 @@ fn parse_create_table_gencol() { mysql_and_generic().verified_stmt("CREATE TABLE t1 (a INT, b INT AS (a * 2) STORED)"); } +#[test] +fn parse_create_table_options_comma_separated() { + let sql = "CREATE TABLE t (x INT) DEFAULT CHARSET = utf8mb4, ENGINE = InnoDB , AUTO_INCREMENT 1 DATA DIRECTORY '/var/lib/mysql/data'"; + let canonical = "CREATE TABLE t (x INT) DEFAULT CHARSET = utf8mb4 ENGINE = InnoDB AUTO_INCREMENT = 1 DATA DIRECTORY = '/var/lib/mysql/data'"; + mysql_and_generic().one_statement_parses_to(sql, canonical); +} + #[test] fn parse_quote_identifiers() { let sql = "CREATE TABLE `PRIMARY` (`BEGIN` INT PRIMARY KEY)"; @@ -1089,6 +1410,7 @@ fn parse_escaped_quote_identifiers_with_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1113,6 +1435,7 @@ fn parse_escaped_quote_identifiers_with_escape() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1126,6 +1449,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { ParserOptions { trailing_commas: false, unescape: false, + require_semicolon_stmt_delimiter: true, } ) .verified_stmt(sql), @@ -1141,6 +1465,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1165,6 +1490,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1187,6 +1513,7 @@ fn parse_escaped_backticks_with_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1211,6 +1538,7 @@ fn parse_escaped_backticks_with_escape() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1237,6 +1565,7 @@ fn parse_escaped_backticks_with_no_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1261,6 +1590,7 @@ fn parse_escaped_backticks_with_no_escape() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1382,6 +1712,51 @@ fn parse_create_table_unsigned() { } } +#[test] +fn parse_signed_data_types() { + let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3) SIGNED, bar_smallint SMALLINT(5) SIGNED, bar_mediumint MEDIUMINT(13) SIGNED, bar_int INT(11) SIGNED, bar_bigint BIGINT(20) SIGNED)"; + let canonical = "CREATE TABLE foo (bar_tinyint TINYINT(3), bar_smallint SMALLINT(5), bar_mediumint MEDIUMINT(13), bar_int INT(11), bar_bigint BIGINT(20))"; + match mysql().one_statement_parses_to(sql, canonical) { + Statement::CreateTable(CreateTable { name, columns, .. }) => { + assert_eq!(name.to_string(), "foo"); + assert_eq!( + vec![ + ColumnDef { + name: Ident::new("bar_tinyint"), + data_type: DataType::TinyInt(Some(3)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_smallint"), + data_type: DataType::SmallInt(Some(5)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_mediumint"), + data_type: DataType::MediumInt(Some(13)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_int"), + data_type: DataType::Int(Some(11)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_bigint"), + data_type: DataType::BigInt(Some(20)), + options: vec![], + }, + ], + columns + ); + } + _ => unreachable!(), + } + all_dialects_except(|d| d.supports_data_type_signed_suffix()) + .run_parser_method(sql, |p| p.parse_statement()) + .expect_err("SIGNED suffix should not be allowed"); +} + #[test] fn parse_simple_insert() { let sql = r"INSERT INTO tasks (title, priority) VALUES ('Test Some Inserts', 1), ('Test Entry 2', 2), ('Test Entry 3', 3)"; @@ -1436,6 +1811,7 @@ fn parse_simple_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1484,6 +1860,7 @@ fn parse_ignore_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1532,6 +1909,7 @@ fn parse_priority_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1577,6 +1955,7 @@ fn parse_priority_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1624,6 +2003,7 @@ fn parse_insert_as() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1686,6 +2066,7 @@ fn parse_insert_as() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1735,6 +2116,7 @@ fn parse_replace_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1775,6 +2157,7 @@ fn parse_empty_row_insert() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1839,6 +2222,7 @@ fn parse_insert_with_on_duplicate_update() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1898,6 +2282,7 @@ fn parse_select_with_numeric_prefix_column_name() { projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new( "123col_$@123abc" )))], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::with_quote( @@ -1935,11 +2320,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { assert_eq!(&[Ident::new("t"), Ident::new("15to29")], &parts[..]); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 2: Qualified column name that starts with digits and on its own represents a number. @@ -1949,11 +2334,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { assert_eq!(&[Ident::new("t"), Ident::new("15e29")], &parts[..]); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 3: Unqualified, the same token is parsed as a number. @@ -1967,11 +2352,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan { value, .. }))) => { assert_eq!(&number("15e29"), value); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 4: Quoted simple identifier. @@ -1981,11 +2366,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::Identifier(name))) => { assert_eq!(&Ident::with_quote('`', "15e29"), name); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 5: Quoted compound identifier. @@ -1998,11 +2383,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { &parts[..] ); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 6: Multi-level compound identifiers. @@ -2019,11 +2404,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { &parts[..] ); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 7: Multi-level compound quoted identifiers. @@ -2040,11 +2425,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { &parts[..] ); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } } @@ -2065,7 +2450,6 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { q.body, Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), - distinct: None, top: None, top_before_distinct: false, @@ -2073,6 +2457,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { SelectItem::UnnamedExpr(Expr::value(number("123e4"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("123col_$@123abc"))) ], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::with_quote( @@ -2131,6 +2516,7 @@ fn parse_update_with_joins() { selection, returning, or: None, + limit: None, } => { assert_eq!( TableWithJoins { @@ -2254,11 +2640,14 @@ fn parse_alter_table_add_column() { if_exists, only, operations, + iceberg, location: _, on_cluster: _, + end_token: _, } => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); + assert!(!iceberg); assert!(!only); assert_eq!( operations, @@ -2283,8 +2672,7 @@ fn parse_alter_table_add_column() { if_exists, only, operations, - location: _, - on_cluster: _, + .. } => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); @@ -2321,8 +2709,7 @@ fn parse_alter_table_add_columns() { if_exists, only, operations, - location: _, - on_cluster: _, + .. } => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); @@ -2365,7 +2752,9 @@ fn parse_alter_table_add_columns() { fn parse_alter_table_drop_primary_key() { assert_matches!( alter_table_op(mysql_and_generic().verified_stmt("ALTER TABLE tab DROP PRIMARY KEY")), - AlterTableOperation::DropPrimaryKey + AlterTableOperation::DropPrimaryKey { + drop_behavior: None + } ); } @@ -2375,7 +2764,7 @@ fn parse_alter_table_drop_foreign_key() { alter_table_op( mysql_and_generic().verified_stmt("ALTER TABLE tab DROP FOREIGN KEY foo_ibfk_1") ), - AlterTableOperation::DropForeignKey { name } if name.value == "foo_ibfk_1" + AlterTableOperation::DropForeignKey { name, .. } if name.value == "foo_ibfk_1" ); } @@ -2548,7 +2937,8 @@ fn parse_alter_table_with_algorithm() { operations, vec![ AlterTableOperation::DropColumn { - column_name: Ident::new("password_digest"), + has_column_keyword: true, + column_names: vec![Ident::new("password_digest")], if_exists: false, drop_behavior: None, }, @@ -2595,7 +2985,8 @@ fn parse_alter_table_with_lock() { operations, vec![ AlterTableOperation::DropColumn { - column_name: Ident::new("password_digest"), + has_column_keyword: true, + column_names: vec![Ident::new("password_digest")], if_exists: false, drop_behavior: None, }, @@ -2714,6 +3105,7 @@ fn parse_substring_in_select() { special: true, shorthand: false, })], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident { @@ -2745,6 +3137,7 @@ fn parse_substring_in_select() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), query ); @@ -3027,6 +3420,7 @@ fn parse_hex_string_introducer() { ) .into(), })], + exclude: None, from: vec![], lateral_views: vec![], prewhere: None, @@ -3051,6 +3445,7 @@ fn parse_hex_string_introducer() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ) } @@ -3283,7 +3678,9 @@ fn parse_grant() { objects, grantees, with_grant_option, + as_grantor: _, granted_by, + current_grants: _, } = stmt { assert_eq!( @@ -3489,6 +3886,11 @@ fn parse_begin_without_transaction() { mysql().verified_stmt("BEGIN"); } +#[test] +fn parse_geometric_types_srid_option() { + mysql_and_generic().verified_stmt("CREATE TABLE t (a geometry SRID 4326)"); +} + #[test] fn parse_double_precision() { mysql().verified_stmt("CREATE TABLE foo (bar DOUBLE)"); @@ -3516,18 +3918,17 @@ fn parse_looks_like_single_line_comment() { #[test] fn parse_create_trigger() { - let sql_create_trigger = r#" - CREATE TRIGGER emp_stamp BEFORE INSERT ON emp - FOR EACH ROW EXECUTE FUNCTION emp_stamp(); - "#; - let create_stmt = mysql().one_statement_parses_to(sql_create_trigger, ""); + let sql_create_trigger = r#"CREATE TRIGGER emp_stamp BEFORE INSERT ON emp FOR EACH ROW EXECUTE FUNCTION emp_stamp()"#; + let create_stmt = mysql().verified_stmt(sql_create_trigger); assert_eq!( create_stmt, Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), period: TriggerPeriod::Before, + period_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, @@ -3535,18 +3936,26 @@ fn parse_create_trigger() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("emp_stamp")]), - args: None, + args: Some(vec![]), } - }, + }), + statements_as: false, + statements: None, characteristics: None, } ); } +#[test] +fn parse_create_trigger_compound_statement() { + mysql_and_generic().verified_stmt("CREATE TRIGGER mytrigger BEFORE INSERT ON mytable FOR EACH ROW BEGIN SET NEW.a = 1; SET NEW.b = 2; END"); + mysql_and_generic().verified_stmt("CREATE TRIGGER tr AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES (NEW.id); END"); +} + #[test] fn parse_drop_trigger() { let sql_drop_trigger = "DROP TRIGGER emp_stamp;"; @@ -3722,3 +4131,119 @@ fn parse_straight_join() { mysql() .verified_stmt("SELECT a.*, b.* FROM table_a STRAIGHT_JOIN table_b AS b ON a.b_id = b.id"); } + +#[test] +fn mysql_foreign_key_with_index_name() { + mysql().verified_stmt( + "CREATE TABLE orders (customer_id INT, INDEX idx_customer (customer_id), CONSTRAINT fk_customer FOREIGN KEY idx_customer (customer_id) REFERENCES customers(id))", + ); +} + +#[test] +fn parse_drop_index() { + let sql = "DROP INDEX idx_name ON table_name"; + match mysql().verified_stmt(sql) { + Statement::Drop { + object_type, + if_exists, + names, + cascade, + restrict, + purge, + temporary, + table, + } => { + assert!(!if_exists); + assert_eq!(ObjectType::Index, object_type); + assert_eq!( + vec!["idx_name"], + names.iter().map(ToString::to_string).collect::>() + ); + assert!(!cascade); + assert!(!restrict); + assert!(!purge); + assert!(!temporary); + assert!(table.is_some()); + assert_eq!("table_name", table.unwrap().to_string()); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_drop_index() { + assert_matches!( + alter_table_op( + mysql_and_generic().verified_stmt("ALTER TABLE tab DROP INDEX idx_index") + ), + AlterTableOperation::DropIndex { name } if name.value == "idx_index" + ); +} + +#[test] +fn parse_json_member_of() { + mysql().verified_stmt(r#"SELECT 17 MEMBER OF('[23, "abc", 17, "ab", 10]')"#); + let sql = r#"SELECT 'ab' MEMBER OF('[23, "abc", 17, "ab", 10]')"#; + let stmt = mysql().verified_stmt(sql); + match stmt { + Statement::Query(query) => { + let select = query.body.as_select().unwrap(); + assert_eq!( + select.projection, + vec![SelectItem::UnnamedExpr(Expr::MemberOf(MemberOf { + value: Box::new(Expr::Value( + Value::SingleQuotedString("ab".to_string()).into() + )), + array: Box::new(Expr::Value( + Value::SingleQuotedString(r#"[23, "abc", 17, "ab", 10]"#.to_string()) + .into() + )), + }))] + ); + } + _ => panic!("Unexpected statement {stmt}"), + } +} + +#[test] +fn parse_show_charset() { + let res = mysql().verified_stmt("SHOW CHARACTER SET"); + assert_eq!( + res, + Statement::ShowCharset(ShowCharset { + is_shorthand: false, + filter: None + }) + ); + mysql().verified_stmt("SHOW CHARACTER SET LIKE 'utf8mb4%'"); + mysql().verified_stmt("SHOW CHARSET WHERE charset = 'utf8mb4%'"); + mysql().verified_stmt("SHOW CHARSET LIKE 'utf8mb4%'"); +} + +#[test] +fn test_ddl_with_index_using() { + let columns = "(name, age DESC)"; + let using = "USING BTREE"; + + for sql in [ + format!("CREATE INDEX idx_name ON test {using} {columns}"), + format!("CREATE TABLE foo (name VARCHAR(255), age INT, KEY idx_name {using} {columns})"), + format!("ALTER TABLE foo ADD KEY idx_name {using} {columns}"), + format!("CREATE INDEX idx_name ON test{columns} {using}"), + format!("CREATE TABLE foo (name VARCHAR(255), age INT, KEY idx_name {columns} {using})"), + format!("ALTER TABLE foo ADD KEY idx_name {columns} {using}"), + ] { + mysql_and_generic().verified_stmt(&sql); + } +} + +#[test] +fn test_create_index_options() { + mysql_and_generic() + .verified_stmt("CREATE INDEX idx_name ON t(c1, c2) USING HASH LOCK = SHARED"); + mysql_and_generic() + .verified_stmt("CREATE INDEX idx_name ON t(c1, c2) USING BTREE ALGORITHM = INPLACE"); + mysql_and_generic().verified_stmt( + "CREATE INDEX idx_name ON t(c1, c2) USING BTREE LOCK = EXCLUSIVE ALGORITHM = DEFAULT", + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 27fc7fa17..43b30aade 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -21,6 +21,7 @@ #[macro_use] mod test_utils; + use helpers::attached_token::AttachedToken; use sqlparser::tokenizer::Span; use test_utils::*; @@ -348,7 +349,7 @@ fn parse_create_table_with_defaults() { name, columns, constraints, - with_options, + table_options, if_not_exists: false, external: false, file_format: None, @@ -485,6 +486,11 @@ fn parse_create_table_with_defaults() { ] ); assert!(constraints.is_empty()); + + let with_options = match table_options { + CreateTableOptions::With(options) => options, + _ => unreachable!(), + }; assert_eq!( with_options, vec![ @@ -600,9 +606,10 @@ fn parse_alter_table_constraints_unique_nulls_distinct() { .verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS NOT DISTINCT (c)") { Statement::AlterTable { operations, .. } => match &operations[0] { - AlterTableOperation::AddConstraint(TableConstraint::Unique { - nulls_distinct, .. - }) => { + AlterTableOperation::AddConstraint { + constraint: TableConstraint::Unique { nulls_distinct, .. }, + .. + } => { assert_eq!(nulls_distinct, &NullsDistinctOption::NotDistinct) } _ => unreachable!(), @@ -758,10 +765,7 @@ fn parse_drop_extension() { #[test] fn parse_alter_table_alter_column() { - pg().one_statement_parses_to( - "ALTER TABLE tab ALTER COLUMN is_active TYPE TEXT USING 'text'", - "ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'", - ); + pg().verified_stmt("ALTER TABLE tab ALTER COLUMN is_active TYPE TEXT USING 'text'"); match alter_table_op( pg().verified_stmt( @@ -777,6 +781,7 @@ fn parse_alter_table_alter_column() { AlterColumnOperation::SetDataType { data_type: DataType::Text, using: Some(using_expr), + had_set: true, } ); } @@ -828,8 +833,7 @@ fn parse_alter_table_add_columns() { if_exists, only, operations, - location: _, - on_cluster: _, + .. } => { assert_eq!(name.to_string(), "tab"); assert!(if_exists); @@ -909,8 +913,7 @@ fn parse_alter_table_owner_to() { if_exists: _, only: _, operations, - location: _, - on_cluster: _, + .. } => { assert_eq!(name.to_string(), "tab"); assert_eq!( @@ -1302,6 +1305,7 @@ fn parse_copy_to() { }, } ], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1326,6 +1330,7 @@ fn parse_copy_to() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), to: true, target: CopyTarget::File { @@ -1661,7 +1666,9 @@ fn parse_execute() { has_parentheses: false, using: vec![], immediate: false, - into: vec![] + into: vec![], + output: false, + default: false, } ); @@ -1677,7 +1684,9 @@ fn parse_execute() { has_parentheses: true, using: vec![], immediate: false, - into: vec![] + into: vec![], + output: false, + default: false, } ); @@ -1714,7 +1723,9 @@ fn parse_execute() { }, ], immediate: false, - into: vec![] + into: vec![], + output: false, + default: false, } ); } @@ -2176,21 +2187,39 @@ fn parse_pg_regex_match_ops() { ("!~*", BinaryOperator::PGRegexNotIMatch), ]; + // Match against a single value for (str_op, op) in pg_regex_match_ops { - let select = pg().verified_only_select(&format!("SELECT 'abc' {} '^a'", &str_op)); + let select = pg().verified_only_select(&format!("SELECT 'abc' {str_op} '^a'")); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value( - (Value::SingleQuotedString("abc".into())).with_empty_span() - )), + left: Box::new(Expr::Value(single_quoted_string("abc").with_empty_span(),)), op: op.clone(), - right: Box::new(Expr::Value( - (Value::SingleQuotedString("^a".into())).with_empty_span() - )), + right: Box::new(Expr::Value(single_quoted_string("^a").with_empty_span(),)), }), select.projection[0] ); } + + // Match against any value from an array + for (str_op, op) in pg_regex_match_ops { + let select = + pg().verified_only_select(&format!("SELECT 'abc' {str_op} ANY(ARRAY['^a', 'x'])")); + assert_eq!( + SelectItem::UnnamedExpr(Expr::AnyOp { + left: Box::new(Expr::Value(single_quoted_string("abc").with_empty_span(),)), + compare_op: op.clone(), + right: Box::new(Expr::Array(Array { + elem: vec![ + Expr::Value(single_quoted_string("^a").with_empty_span()), + Expr::Value(single_quoted_string("x").with_empty_span()), + ], + named: true, + })), + is_some: false, + }), + select.projection[0] + ) + } } #[test] @@ -2202,21 +2231,35 @@ fn parse_pg_like_match_ops() { ("!~~*", BinaryOperator::PGNotILikeMatch), ]; + // Match against a single value for (str_op, op) in pg_like_match_ops { - let select = pg().verified_only_select(&format!("SELECT 'abc' {} 'a_c%'", &str_op)); + let select = pg().verified_only_select(&format!("SELECT 'abc' {str_op} 'a_c%'")); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value( - (Value::SingleQuotedString("abc".into())).with_empty_span() - )), + left: Box::new(Expr::Value(single_quoted_string("abc").with_empty_span(),)), op: op.clone(), - right: Box::new(Expr::Value( - (Value::SingleQuotedString("a_c%".into())).with_empty_span() - )), + right: Box::new(Expr::Value(single_quoted_string("a_c%").with_empty_span(),)), }), select.projection[0] ); } + + // Match against all values from an array + for (str_op, op) in pg_like_match_ops { + let select = + pg().verified_only_select(&format!("SELECT 'abc' {str_op} ALL(ARRAY['a_c%'])")); + assert_eq!( + SelectItem::UnnamedExpr(Expr::AllOp { + left: Box::new(Expr::Value(single_quoted_string("abc").with_empty_span(),)), + compare_op: op.clone(), + right: Box::new(Expr::Array(Array { + elem: vec![Expr::Value(single_quoted_string("a_c%").with_empty_span())], + named: true, + })), + }), + select.projection[0] + ) + } } #[test] @@ -2443,7 +2486,7 @@ fn parse_array_multi_subscript() { #[test] fn parse_create_index() { - let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2)"; + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2)"; match pg().verified_stmt(sql) { Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), @@ -2457,6 +2500,8 @@ fn parse_create_index() { include, with, predicate: None, + index_options, + alter_options, }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); @@ -2467,6 +2512,8 @@ fn parse_create_index() { assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2474,7 +2521,7 @@ fn parse_create_index() { #[test] fn parse_create_anonymous_index() { - let sql = "CREATE INDEX ON my_table(col1,col2)"; + let sql = "CREATE INDEX ON my_table(col1, col2)"; match pg().verified_stmt(sql) { Statement::CreateIndex(CreateIndex { name, @@ -2488,6 +2535,8 @@ fn parse_create_anonymous_index() { nulls_distinct: None, with, predicate: None, + index_options, + alter_options, }) => { assert_eq!(None, name); assert_eq_vec(&["my_table"], &table_name); @@ -2498,6 +2547,8 @@ fn parse_create_anonymous_index() { assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2530,12 +2581,12 @@ fn parse_create_indices_with_operator_classes() { for expected_operator_class in &operator_classes { let single_column_sql_statement = format!( "CREATE INDEX the_index_name ON users USING {expected_index_type} (concat_users_name(first_name, last_name){})", - expected_operator_class.as_ref().map(|oc| format!(" {}", oc)) + expected_operator_class.as_ref().map(|oc| format!(" {oc}")) .unwrap_or_default() ); let multi_column_sql_statement = format!( - "CREATE INDEX the_index_name ON users USING {expected_index_type} (column_name,concat_users_name(first_name, last_name){})", - expected_operator_class.as_ref().map(|oc| format!(" {}", oc)) + "CREATE INDEX the_index_name ON users USING {expected_index_type} (column_name, concat_users_name(first_name, last_name){})", + expected_operator_class.as_ref().map(|oc| format!(" {oc}")) .unwrap_or_default() ); @@ -2596,6 +2647,8 @@ fn parse_create_indices_with_operator_classes() { nulls_distinct: None, with, predicate: None, + index_options, + alter_options, }) => { assert_eq_vec(&["the_index_name"], &name); assert_eq_vec(&["users"], &table_name); @@ -2603,6 +2656,8 @@ fn parse_create_indices_with_operator_classes() { assert_eq!(expected_function_column, columns[0],); assert!(include.is_empty()); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2620,6 +2675,8 @@ fn parse_create_indices_with_operator_classes() { nulls_distinct: None, with, predicate: None, + index_options, + alter_options, }) => { assert_eq_vec(&["the_index_name"], &name); assert_eq_vec(&["users"], &table_name); @@ -2645,6 +2702,8 @@ fn parse_create_indices_with_operator_classes() { assert_eq!(expected_function_column, columns[1],); assert!(include.is_empty()); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2655,7 +2714,7 @@ fn parse_create_indices_with_operator_classes() { #[test] fn parse_create_bloom() { let sql = - "CREATE INDEX bloomidx ON tbloom USING BLOOM (i1,i2,i3) WITH (length = 80, col1 = 2, col2 = 2, col3 = 4)"; + "CREATE INDEX bloomidx ON tbloom USING BLOOM (i1, i2, i3) WITH (length = 80, col1 = 2, col2 = 2, col3 = 4)"; match pg().verified_stmt(sql) { Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), @@ -2669,6 +2728,8 @@ fn parse_create_bloom() { nulls_distinct: None, with, predicate: None, + index_options, + alter_options, }) => { assert_eq_vec(&["bloomidx"], &name); assert_eq_vec(&["tbloom"], &table_name); @@ -2700,6 +2761,8 @@ fn parse_create_bloom() { ], with ); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2721,6 +2784,8 @@ fn parse_create_brin() { nulls_distinct: None, with, predicate: None, + index_options, + alter_options, }) => { assert_eq_vec(&["brin_sensor_data_recorded_at"], &name); assert_eq_vec(&["sensor_data"], &table_name); @@ -2728,6 +2793,8 @@ fn parse_create_brin() { assert_eq_vec(&["recorded_at"], &columns); assert!(include.is_empty()); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2770,7 +2837,7 @@ fn parse_create_table_with_empty_inherits_fails() { #[test] fn parse_create_index_concurrently() { - let sql = "CREATE INDEX CONCURRENTLY IF NOT EXISTS my_index ON my_table(col1,col2)"; + let sql = "CREATE INDEX CONCURRENTLY IF NOT EXISTS my_index ON my_table(col1, col2)"; match pg().verified_stmt(sql) { Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), @@ -2784,6 +2851,8 @@ fn parse_create_index_concurrently() { nulls_distinct: None, with, predicate: None, + index_options, + alter_options, }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); @@ -2794,6 +2863,8 @@ fn parse_create_index_concurrently() { assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2801,7 +2872,7 @@ fn parse_create_index_concurrently() { #[test] fn parse_create_index_with_predicate() { - let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) WHERE col3 IS NULL"; + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) WHERE col3 IS NULL"; match pg().verified_stmt(sql) { Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), @@ -2815,6 +2886,8 @@ fn parse_create_index_with_predicate() { nulls_distinct: None, with, predicate: Some(_), + index_options, + alter_options, }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); @@ -2825,6 +2898,8 @@ fn parse_create_index_with_predicate() { assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2832,7 +2907,7 @@ fn parse_create_index_with_predicate() { #[test] fn parse_create_index_with_include() { - let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) INCLUDE (col3)"; + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) INCLUDE (col3, col4)"; match pg().verified_stmt(sql) { Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), @@ -2846,6 +2921,8 @@ fn parse_create_index_with_include() { nulls_distinct: None, with, predicate: None, + index_options, + alter_options, }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); @@ -2854,8 +2931,10 @@ fn parse_create_index_with_include() { assert!(!concurrently); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); - assert_eq_vec(&["col3"], &include); + assert_eq_vec(&["col3", "col4"], &include); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2863,7 +2942,7 @@ fn parse_create_index_with_include() { #[test] fn parse_create_index_with_nulls_distinct() { - let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) NULLS NOT DISTINCT"; + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) NULLS NOT DISTINCT"; match pg().verified_stmt(sql) { Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), @@ -2877,6 +2956,8 @@ fn parse_create_index_with_nulls_distinct() { nulls_distinct: Some(nulls_distinct), with, predicate: None, + index_options, + alter_options, }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); @@ -2888,11 +2969,13 @@ fn parse_create_index_with_nulls_distinct() { assert!(include.is_empty()); assert!(!nulls_distinct); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } - let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) NULLS DISTINCT"; + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) NULLS DISTINCT"; match pg().verified_stmt(sql) { Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), @@ -2906,6 +2989,8 @@ fn parse_create_index_with_nulls_distinct() { nulls_distinct: Some(nulls_distinct), with, predicate: None, + index_options, + alter_options, }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); @@ -2917,6 +3002,8 @@ fn parse_create_index_with_nulls_distinct() { assert!(include.is_empty()); assert!(nulls_distinct); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -2944,6 +3031,7 @@ fn parse_array_subquery_expr() { projection: vec![SelectItem::UnnamedExpr(Expr::Value( (number("1")).with_empty_span() ))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2969,6 +3057,7 @@ fn parse_array_subquery_expr() { projection: vec![SelectItem::UnnamedExpr(Expr::Value( (number("2")).with_empty_span() ))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2994,6 +3083,7 @@ fn parse_array_subquery_expr() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), filter: None, null_treatment: None, @@ -3267,7 +3357,7 @@ fn test_fn_arg_with_value_operator() { assert!(matches!( &args[..], &[FunctionArg::ExprNamed { operator: FunctionArgOperator::Value, .. }] - ), "Invalid function argument: {:?}", args); + ), "Invalid function argument: {args:?}"); } other => panic!("Expected: JSON_OBJECT('name' VALUE 'value') to be parsed as a function, but got {other:?}"), } @@ -4098,6 +4188,216 @@ fn parse_update_in_with_subquery() { pg_and_generic().verified_stmt(r#"WITH "result" AS (UPDATE "Hero" SET "name" = 'Captain America', "number_of_movies" = "number_of_movies" + 1 WHERE "secret_identity" = 'Sam Wilson' RETURNING "id", "name", "secret_identity", "number_of_movies") SELECT * FROM "result""#); } +#[test] +fn parser_create_function_with_args() { + let sql1 = r#"CREATE OR REPLACE FUNCTION check_strings_different(str1 VARCHAR, str2 VARCHAR) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +BEGIN + IF str1 <> str2 THEN + RETURN TRUE; + ELSE + RETURN FALSE; + END IF; +END; +$$"#; + + assert_eq!( + pg_and_generic().verified_stmt(sql1), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("check_strings_different")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "str1", + DataType::Varchar(None), + ), + OperateFunctionArg::with_name( + "str2", + DataType::Varchar(None), + ), + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF str1 <> str2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let sql2 = r#"CREATE OR REPLACE FUNCTION check_not_zero(int1 INT) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +BEGIN + IF int1 <> 0 THEN + RETURN TRUE; + ELSE + RETURN FALSE; + END IF; +END; +$$"#; + assert_eq!( + pg_and_generic().verified_stmt(sql2), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("check_not_zero")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "int1", + DataType::Int(None) + ) + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> 0 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let sql3 = r#"CREATE OR REPLACE FUNCTION check_values_different(a INT, b INT) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +BEGIN + IF a <> b THEN + RETURN TRUE; + ELSE + RETURN FALSE; + END IF; +END; +$$"#; + assert_eq!( + pg_and_generic().verified_stmt(sql3), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("check_values_different")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "a", + DataType::Int(None) + ), + OperateFunctionArg::with_name( + "b", + DataType::Int(None) + ), + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF a <> b THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let sql4 = r#"CREATE OR REPLACE FUNCTION check_values_different(int1 INT, int2 INT) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +BEGIN + IF int1 <> int2 THEN + RETURN TRUE; + ELSE + RETURN FALSE; + END IF; +END; +$$"#; + assert_eq!( + pg_and_generic().verified_stmt(sql4), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("check_values_different")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "int1", + DataType::Int(None) + ), + OperateFunctionArg::with_name( + "int2", + DataType::Int(None) + ), + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> int2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let sql5 = r#"CREATE OR REPLACE FUNCTION foo(a TIMESTAMP WITH TIME ZONE, b VARCHAR) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ + BEGIN + RETURN TRUE; + END; + $$"#; + assert_eq!( + pg_and_generic().verified_stmt(sql5), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: true, + temporary: false, + name: ObjectName::from(vec![Ident::new("foo")]), + args: Some(vec![ + OperateFunctionArg::with_name( + "a", + DataType::Timestamp(None, TimezoneInfo::WithTimeZone) + ), + OperateFunctionArg::with_name("b", DataType::Varchar(None)), + ]), + return_type: Some(DataType::Boolean), + language: Some("plpgsql".into()), + behavior: None, + called_on_null: None, + parallel: None, + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + value: "\n BEGIN\n RETURN TRUE;\n END;\n ".to_owned(), + tag: None + })) + .with_empty_span() + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + let incorrect_sql = "CREATE FUNCTION add(function(struct int64), b INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'"; + assert!(pg().parse_sql_statements(incorrect_sql).is_err(),); +} + #[test] fn parse_create_function() { let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'"; @@ -4239,6 +4539,66 @@ fn parse_drop_function() { ); } +#[test] +fn parse_drop_domain() { + let sql = "DROP DOMAIN IF EXISTS jpeg_domain"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropDomain(DropDomain { + if_exists: true, + name: ObjectName::from(vec![Ident { + value: "jpeg_domain".to_string(), + quote_style: None, + span: Span::empty(), + }]), + drop_behavior: None + }) + ); + + let sql = "DROP DOMAIN jpeg_domain"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropDomain(DropDomain { + if_exists: false, + name: ObjectName::from(vec![Ident { + value: "jpeg_domain".to_string(), + quote_style: None, + span: Span::empty(), + }]), + drop_behavior: None + }) + ); + + let sql = "DROP DOMAIN IF EXISTS jpeg_domain CASCADE"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropDomain(DropDomain { + if_exists: true, + name: ObjectName::from(vec![Ident { + value: "jpeg_domain".to_string(), + quote_style: None, + span: Span::empty(), + }]), + drop_behavior: Some(DropBehavior::Cascade) + }) + ); + + let sql = "DROP DOMAIN IF EXISTS jpeg_domain RESTRICT"; + + assert_eq!( + pg().verified_stmt(sql), + Statement::DropDomain(DropDomain { + if_exists: true, + name: ObjectName::from(vec![Ident { + value: "jpeg_domain".to_string(), + quote_style: None, + span: Span::empty(), + }]), + drop_behavior: Some(DropBehavior::Restrict) + }) + ); +} + #[test] fn parse_drop_procedure() { let sql = "DROP PROCEDURE IF EXISTS test_proc"; @@ -4407,7 +4767,7 @@ fn parse_dollar_quoted_string() { quote_style: None, span: Span::empty(), }, - } + }, ); assert_eq!( @@ -4512,13 +4872,13 @@ fn parse_truncate() { let table_name = ObjectName::from(vec![Ident::new("db"), Ident::new("table_name")]); let table_names = vec![TruncateTableTarget { name: table_name.clone(), + only: false, }]; assert_eq!( Statement::Truncate { table_names, partitions: None, table: false, - only: false, identity: None, cascade: None, on_cluster: None, @@ -4535,6 +4895,7 @@ fn parse_truncate_with_options() { let table_name = ObjectName::from(vec![Ident::new("db"), Ident::new("table_name")]); let table_names = vec![TruncateTableTarget { name: table_name.clone(), + only: true, }]; assert_eq!( @@ -4542,7 +4903,6 @@ fn parse_truncate_with_options() { table_names, partitions: None, table: true, - only: true, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, @@ -4563,9 +4923,11 @@ fn parse_truncate_with_table_list() { let table_names = vec![ TruncateTableTarget { name: table_name_a.clone(), + only: false, }, TruncateTableTarget { name: table_name_b.clone(), + only: false, }, ]; @@ -4574,7 +4936,6 @@ fn parse_truncate_with_table_list() { table_names, partitions: None, table: true, - only: false, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, @@ -4606,7 +4967,6 @@ fn parse_create_table_with_alias() { name, columns, constraints, - with_options: _with_options, if_not_exists: false, external: false, file_format: None, @@ -4785,6 +5145,7 @@ fn test_simple_postgres_insert_with_alias() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), assignments: vec![], partitioned: None, @@ -4856,6 +5217,7 @@ fn test_simple_postgres_insert_with_alias() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), assignments: vec![], partitioned: None, @@ -4925,6 +5287,7 @@ fn test_simple_insert_with_quoted_alias() { for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), assignments: vec![], partitioned: None, @@ -4977,10 +5340,14 @@ fn parse_at_time_zone() { // check precedence let expr = Expr::BinaryOp { left: Box::new(Expr::AtTimeZone { - timestamp: Box::new(Expr::TypedString { + timestamp: Box::new(Expr::TypedString(TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("2001-09-28 01:00".to_string()), - }), + value: ValueWithSpan { + value: Value::SingleQuotedString("2001-09-28 01:00".to_string()), + span: Span::empty(), + }, + uses_odbc_syntax: false, + })), time_zone: Box::new(Expr::Cast { kind: CastKind::DoubleColon, expr: Box::new(Expr::Value( @@ -5009,11 +5376,53 @@ fn parse_at_time_zone() { ); } +#[test] +fn parse_interval_data_type() { + pg_and_generic().verified_stmt("CREATE TABLE t (i INTERVAL)"); + for p in 0..=6 { + pg_and_generic().verified_stmt(&format!("CREATE TABLE t (i INTERVAL({p}))")); + pg_and_generic().verified_stmt(&format!("SELECT '1 second'::INTERVAL({p})")); + pg_and_generic().verified_stmt(&format!("SELECT CAST('1 second' AS INTERVAL({p}))")); + } + let fields = [ + "YEAR", + "MONTH", + "DAY", + "HOUR", + "MINUTE", + "SECOND", + "YEAR TO MONTH", + "DAY TO HOUR", + "DAY TO MINUTE", + "DAY TO SECOND", + "HOUR TO MINUTE", + "HOUR TO SECOND", + "MINUTE TO SECOND", + ]; + for field in fields { + pg_and_generic().verified_stmt(&format!("CREATE TABLE t (i INTERVAL {field})")); + pg_and_generic().verified_stmt(&format!("SELECT '1 second'::INTERVAL {field}")); + pg_and_generic().verified_stmt(&format!("SELECT CAST('1 second' AS INTERVAL {field})")); + } + for p in 0..=6 { + for field in fields { + pg_and_generic().verified_stmt(&format!("CREATE TABLE t (i INTERVAL {field}({p}))")); + pg_and_generic().verified_stmt(&format!("SELECT '1 second'::INTERVAL {field}({p})")); + pg_and_generic() + .verified_stmt(&format!("SELECT CAST('1 second' AS INTERVAL {field}({p}))")); + } + } +} + #[test] fn parse_create_table_with_options() { let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)"; match pg().verified_stmt(sql) { - Statement::CreateTable(CreateTable { with_options, .. }) => { + Statement::CreateTable(CreateTable { table_options, .. }) => { + let with_options = match table_options { + CreateTableOptions::With(options) => options, + _ => unreachable!(), + }; assert_eq!( vec![ SqlOption::KeyValue { @@ -5080,14 +5489,114 @@ fn test_escaped_string_literal() { } } +#[test] +fn parse_create_domain() { + let sql1 = "CREATE DOMAIN my_domain AS INTEGER CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: None, + default: None, + constraints: vec![TableConstraint::Check { + name: None, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + enforced: None, + }], + }); + + assert_eq!(pg().verified_stmt(sql1), expected); + + let sql2 = "CREATE DOMAIN my_domain AS INTEGER COLLATE \"en_US\" CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: Some(Ident::with_quote('"', "en_US")), + default: None, + constraints: vec![TableConstraint::Check { + name: None, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + enforced: None, + }], + }); + + assert_eq!(pg().verified_stmt(sql2), expected); + + let sql3 = "CREATE DOMAIN my_domain AS INTEGER DEFAULT 1 CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: None, + default: Some(Expr::Value(test_utils::number("1").into())), + constraints: vec![TableConstraint::Check { + name: None, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + enforced: None, + }], + }); + + assert_eq!(pg().verified_stmt(sql3), expected); + + let sql4 = "CREATE DOMAIN my_domain AS INTEGER COLLATE \"en_US\" DEFAULT 1 CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: Some(Ident::with_quote('"', "en_US")), + default: Some(Expr::Value(test_utils::number("1").into())), + constraints: vec![TableConstraint::Check { + name: None, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + enforced: None, + }], + }); + + assert_eq!(pg().verified_stmt(sql4), expected); + + let sql5 = "CREATE DOMAIN my_domain AS INTEGER CONSTRAINT my_constraint CHECK (VALUE > 0)"; + let expected = Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("my_domain")]), + data_type: DataType::Integer(None), + collation: None, + default: None, + constraints: vec![TableConstraint::Check { + name: Some(Ident::new("my_constraint")), + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(test_utils::number("0").into())), + }), + enforced: None, + }], + }); + + assert_eq!(pg().verified_stmt(sql5), expected); +} + #[test] fn parse_create_simple_before_insert_trigger() { let sql = "CREATE TRIGGER check_insert BEFORE INSERT ON accounts FOR EACH ROW EXECUTE FUNCTION check_account_insert"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_insert")]), period: TriggerPeriod::Before, + period_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, @@ -5095,13 +5604,15 @@ fn parse_create_simple_before_insert_trigger() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_insert")]), args: None, }, - }, + }), + statements_as: false, + statements: None, characteristics: None, }; @@ -5112,10 +5623,12 @@ fn parse_create_simple_before_insert_trigger() { fn parse_create_after_update_trigger_with_condition() { let sql = "CREATE TRIGGER check_update AFTER UPDATE ON accounts FOR EACH ROW WHEN (NEW.balance > 10000) EXECUTE FUNCTION check_account_update"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_update")]), period: TriggerPeriod::After, + period_before_table: true, events: vec![TriggerEvent::Update(vec![])], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, @@ -5130,13 +5643,15 @@ fn parse_create_after_update_trigger_with_condition() { op: BinaryOperator::Gt, right: Box::new(Expr::value(number("10000"))), }))), - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_update")]), args: None, }, - }, + }), + statements_as: false, + statements: None, characteristics: None, }; @@ -5147,10 +5662,12 @@ fn parse_create_after_update_trigger_with_condition() { fn parse_create_instead_of_delete_trigger() { let sql = "CREATE TRIGGER check_delete INSTEAD OF DELETE ON accounts FOR EACH ROW EXECUTE FUNCTION check_account_deletes"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_delete")]), period: TriggerPeriod::InsteadOf, + period_before_table: true, events: vec![TriggerEvent::Delete], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, @@ -5158,13 +5675,15 @@ fn parse_create_instead_of_delete_trigger() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_deletes")]), args: None, }, - }, + }), + statements_as: false, + statements: None, characteristics: None, }; @@ -5175,10 +5694,12 @@ fn parse_create_instead_of_delete_trigger() { fn parse_create_trigger_with_multiple_events_and_deferrable() { let sql = "CREATE CONSTRAINT TRIGGER check_multiple_events BEFORE INSERT OR UPDATE OR DELETE ON accounts DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION check_account_changes"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: true, name: ObjectName::from(vec![Ident::new("check_multiple_events")]), period: TriggerPeriod::Before, + period_before_table: true, events: vec![ TriggerEvent::Insert, TriggerEvent::Update(vec![]), @@ -5190,13 +5711,15 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_changes")]), args: None, }, - }, + }), + statements_as: false, + statements: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Deferred), @@ -5211,10 +5734,12 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { fn parse_create_trigger_with_referencing() { let sql = "CREATE TRIGGER check_referencing BEFORE INSERT ON accounts REFERENCING NEW TABLE AS new_accounts OLD TABLE AS old_accounts FOR EACH ROW EXECUTE FUNCTION check_account_referencing"; let expected = Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_referencing")]), period: TriggerPeriod::Before, + period_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, @@ -5233,13 +5758,15 @@ fn parse_create_trigger_with_referencing() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("check_account_referencing")]), args: None, }, - }, + }), + statements_as: false, + statements: None, characteristics: None, }; @@ -5259,7 +5786,7 @@ fn parse_create_trigger_invalid_cases() { ), ( "CREATE TRIGGER check_update TOMORROW UPDATE ON accounts EXECUTE FUNCTION check_account_update", - "Expected: one of BEFORE or AFTER or INSTEAD, found: TOMORROW" + "Expected: one of FOR or BEFORE or AFTER or INSTEAD, found: TOMORROW" ), ( "CREATE TRIGGER check_update BEFORE SAVE ON accounts EXECUTE FUNCTION check_account_update", @@ -5288,7 +5815,7 @@ fn parse_drop_trigger() { "DROP TRIGGER{} check_update ON table_name{}", if if_exists { " IF EXISTS" } else { "" }, option - .map(|o| format!(" {}", o)) + .map(|o| format!(" {o}")) .unwrap_or_else(|| "".to_string()) ); assert_eq!( @@ -5382,8 +5909,7 @@ fn parse_trigger_related_functions() { // Now we parse the statements and check if they are parsed correctly. let mut statements = pg() .parse_sql_statements(&format!( - "{}{}{}{}", - sql_table_creation, sql_create_function, sql_create_trigger, sql_drop_trigger + "{sql_table_creation}{sql_create_function}{sql_create_trigger}{sql_drop_trigger}" )) .unwrap(); @@ -5406,6 +5932,7 @@ fn parse_trigger_related_functions() { temporary: false, external: false, global: None, + dynamic: false, if_not_exists: false, transient: false, volatile: false, @@ -5441,19 +5968,13 @@ fn parse_trigger_related_functions() { storage: None, location: None }), - table_properties: vec![], - with_options: vec![], file_format: None, location: None, query: None, without_rowid: false, like: None, clone: None, - engine: None, comment: None, - auto_increment_offset: None, - default_charset: None, - collation: None, on_commit: None, on_cluster: None, primary_key: None, @@ -5461,7 +5982,6 @@ fn parse_trigger_related_functions() { partition_by: None, cluster_by: None, clustered_by: None, - options: None, inherits: None, strict: false, copy_grants: false, @@ -5478,6 +5998,13 @@ fn parse_trigger_related_functions() { catalog: None, catalog_sync: None, storage_serialization_policy: None, + table_options: CreateTableOptions::None, + target_lag: None, + warehouse: None, + version: None, + refresh_mode: None, + initialize: None, + require_user: false, } ); @@ -5523,10 +6050,12 @@ fn parse_trigger_related_functions() { assert_eq!( create_trigger, Statement::CreateTrigger { + or_alter: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), period: TriggerPeriod::Before, + period_before_table: true, events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![])], table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, @@ -5534,13 +6063,15 @@ fn parse_trigger_related_functions() { trigger_object: TriggerObject::Row, include_each: true, condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { name: ObjectName::from(vec![Ident::new("emp_stamp")]), - args: None, + args: Some(vec![]), } - }, + }), + statements_as: false, + statements: None, characteristics: None } ); @@ -5787,3 +6318,180 @@ fn parse_varbit_datatype() { _ => unreachable!(), } } + +#[test] +fn parse_alter_table_replica_identity() { + match pg_and_generic().verified_stmt("ALTER TABLE foo REPLICA IDENTITY FULL") { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::ReplicaIdentity { + identity: ReplicaIdentity::Full + }] + ); + } + _ => unreachable!(), + } + + match pg_and_generic().verified_stmt("ALTER TABLE foo REPLICA IDENTITY USING INDEX foo_idx") { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::ReplicaIdentity { + identity: ReplicaIdentity::Index("foo_idx".into()) + }] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_ts_datatypes() { + match pg_and_generic().verified_stmt("CREATE TABLE foo (x TSVECTOR)") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "x".into(), + data_type: DataType::TsVector, + options: vec![], + }] + ); + } + _ => unreachable!(), + } + + match pg_and_generic().verified_stmt("CREATE TABLE foo (x TSQUERY)") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "x".into(), + data_type: DataType::TsQuery, + options: vec![], + }] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_constraint_not_valid() { + match pg_and_generic().verified_stmt( + "ALTER TABLE foo ADD CONSTRAINT bar FOREIGN KEY (baz) REFERENCES other(ref) NOT VALID", + ) { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::AddConstraint { + constraint: TableConstraint::ForeignKey { + name: Some("bar".into()), + index_name: None, + columns: vec!["baz".into()], + foreign_table: ObjectName::from(vec!["other".into()]), + referred_columns: vec!["ref".into()], + on_delete: None, + on_update: None, + characteristics: None, + }, + not_valid: true, + }] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_validate_constraint() { + match pg_and_generic().verified_stmt("ALTER TABLE foo VALIDATE CONSTRAINT bar") { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::ValidateConstraint { name: "bar".into() }] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_server() { + let test_cases = vec![ + ( + "CREATE SERVER myserver FOREIGN DATA WRAPPER postgres_fdw", + CreateServerStatement { + name: ObjectName::from(vec!["myserver".into()]), + if_not_exists: false, + server_type: None, + version: None, + foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]), + options: None, + }, + ), + ( + "CREATE SERVER IF NOT EXISTS myserver TYPE 'server_type' VERSION 'server_version' FOREIGN DATA WRAPPER postgres_fdw", + CreateServerStatement { + name: ObjectName::from(vec!["myserver".into()]), + if_not_exists: true, + server_type: Some(Ident { + value: "server_type".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + version: Some(Ident { + value: "server_version".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]), + options: None, + } + ), + ( + "CREATE SERVER myserver2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'foo', dbname 'foodb', port '5432')", + CreateServerStatement { + name: ObjectName::from(vec!["myserver2".into()]), + if_not_exists: false, + server_type: None, + version: None, + foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]), + options: Some(vec![ + CreateServerOption { + key: "host".into(), + value: Ident { + value: "foo".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + CreateServerOption { + key: "dbname".into(), + value: Ident { + value: "foodb".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + CreateServerOption { + key: "port".into(), + value: Ident { + value: "5432".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + ]), + } + ) + ]; + + for (sql, expected) in test_cases { + let Statement::CreateServer(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt, expected); + } +} diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index be2b67223..90652ff48 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -402,3 +402,53 @@ fn parse_extract_single_quotes() { fn parse_string_literal_backslash_escape() { redshift().one_statement_parses_to(r#"SELECT 'l\'auto'"#, "SELECT 'l''auto'"); } + +#[test] +fn parse_utf8_multibyte_idents() { + redshift().verified_stmt("SELECT 🚀.city AS 🎸 FROM customers AS 🚀"); +} + +#[test] +fn parse_vacuum() { + let stmt = redshift().verified_stmt("VACUUM FULL"); + match stmt { + Statement::Vacuum(v) => { + assert!(v.full); + assert_eq!(v.table_name, None); + } + _ => unreachable!(), + } + let stmt = redshift().verified_stmt("VACUUM tbl"); + match stmt { + Statement::Vacuum(v) => { + assert_eq!( + v.table_name, + Some(ObjectName::from(vec![Ident::new("tbl"),])) + ); + } + _ => unreachable!(), + } + let stmt = redshift().verified_stmt( + "VACUUM FULL SORT ONLY DELETE ONLY REINDEX RECLUSTER db1.sc1.tbl1 TO 20 PERCENT BOOST", + ); + match stmt { + Statement::Vacuum(v) => { + assert!(v.full); + assert!(v.sort_only); + assert!(v.delete_only); + assert!(v.reindex); + assert!(v.recluster); + assert_eq!( + v.table_name, + Some(ObjectName::from(vec![ + Ident::new("db1"), + Ident::new("sc1"), + Ident::new("tbl1"), + ])) + ); + assert_eq!(v.threshold, Some(number("20"))); + assert!(v.boost); + } + _ => unreachable!(), + } +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index aa974115d..0448e0c46 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -270,8 +270,8 @@ fn test_snowflake_create_table_with_tag() { assert_eq!("my_table", name.to_string()); assert_eq!( Some(vec![ - Tag::new("A".into(), "TAG A".to_string()), - Tag::new("B".into(), "TAG B".to_string()) + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".to_string()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".to_string()) ]), with_tags ); @@ -291,8 +291,8 @@ fn test_snowflake_create_table_with_tag() { assert_eq!("my_table", name.to_string()); assert_eq!( Some(vec![ - Tag::new("A".into(), "TAG A".to_string()), - Tag::new("B".into(), "TAG B".to_string()) + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".to_string()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".to_string()) ]), with_tags ); @@ -446,19 +446,56 @@ fn test_snowflake_create_table_if_not_exists() { } _ => unreachable!(), } + + for (sql, parse_to) in [ + ( + r#"CREATE TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + r#"CREATE TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + ), + ( + r#"CREATE TABLE "A"."B"."C" IF NOT EXISTS (v VARIANT)"#, + r#"CREATE TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + ), + ( + r#"CREATE TRANSIENT TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + r#"CREATE TRANSIENT TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + ), + ( + r#"CREATE TRANSIENT TABLE "A"."B"."C" IF NOT EXISTS (v VARIANT)"#, + r#"CREATE TRANSIENT TABLE IF NOT EXISTS "A"."B"."C" (v VARIANT)"#, + ), + ] { + snowflake().one_statement_parses_to(sql, parse_to); + } } #[test] fn test_snowflake_create_table_cluster_by() { - match snowflake().verified_stmt("CREATE TABLE my_table (a INT) CLUSTER BY (a, b)") { + match snowflake().verified_stmt("CREATE TABLE my_table (a INT) CLUSTER BY (a, b, my_func(c))") { Statement::CreateTable(CreateTable { name, cluster_by, .. }) => { assert_eq!("my_table", name.to_string()); assert_eq!( Some(WrappedCollection::Parentheses(vec![ - Ident::new("a"), - Ident::new("b"), + Expr::Identifier(Ident::new("a")), + Expr::Identifier(Ident::new("b")), + Expr::Function(Function { + name: ObjectName::from(vec![Ident::new("my_func")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("c")) + ))], + duplicate_treatment: None, + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }), ])), cluster_by ) @@ -470,31 +507,27 @@ fn test_snowflake_create_table_cluster_by() { #[test] fn test_snowflake_create_table_comment() { match snowflake().verified_stmt("CREATE TABLE my_table (a INT) COMMENT = 'some comment'") { - Statement::CreateTable(CreateTable { name, comment, .. }) => { + Statement::CreateTable(CreateTable { + name, + table_options, + .. + }) => { assert_eq!("my_table", name.to_string()); - assert_eq!("some comment", comment.unwrap().to_string()); + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + let comment = match plain_options.first().unwrap() { + SqlOption::Comment(CommentDef::WithEq(c)) + | SqlOption::Comment(CommentDef::WithoutEq(c)) => c, + _ => unreachable!(), + }; + assert_eq!("some comment", comment); } _ => unreachable!(), } } -#[test] -fn test_snowflake_create_table_incomplete_statement() { - assert_eq!( - snowflake().parse_sql_statements("CREATE TABLE my_table"), - Err(ParserError::ParserError( - "unexpected end of input".to_string() - )) - ); - - assert_eq!( - snowflake().parse_sql_statements("CREATE TABLE my_table; (c int)"), - Err(ParserError::ParserError( - "unexpected end of input".to_string() - )) - ); -} - #[test] fn test_snowflake_single_line_tokenize() { let sql = "CREATE TABLE# this is a comment \ntable_1"; @@ -681,7 +714,7 @@ fn test_snowflake_create_table_with_columns_masking_policy() { option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( ColumnPolicyProperty { with, - policy_name: "p".into(), + policy_name: ObjectName::from(vec![Ident::new("p")]), using_columns, } )) @@ -715,7 +748,7 @@ fn test_snowflake_create_table_with_columns_projection_policy() { option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( ColumnPolicyProperty { with, - policy_name: "p".into(), + policy_name: ObjectName::from(vec![Ident::new("p")]), using_columns: None, } )) @@ -752,8 +785,14 @@ fn test_snowflake_create_table_with_columns_tags() { option: ColumnOption::Tags(TagsColumnOption { with, tags: vec![ - Tag::new("A".into(), "TAG A".into()), - Tag::new("B".into(), "TAG B".into()), + Tag::new( + ObjectName::from(vec![Ident::new("A")]), + "TAG A".into() + ), + Tag::new( + ObjectName::from(vec![Ident::new("B")]), + "TAG B".into() + ), ] }), }], @@ -796,7 +835,7 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( ColumnPolicyProperty { with: true, - policy_name: "p1".into(), + policy_name: ObjectName::from(vec![Ident::new("p1")]), using_columns: Some(vec!["a".into(), "b".into()]), } )), @@ -806,8 +845,14 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Tags(TagsColumnOption { with: true, tags: vec![ - Tag::new("A".into(), "TAG A".into()), - Tag::new("B".into(), "TAG B".into()), + Tag::new( + ObjectName::from(vec![Ident::new("A")]), + "TAG A".into() + ), + Tag::new( + ObjectName::from(vec![Ident::new("B")]), + "TAG B".into() + ), ] }), } @@ -828,7 +873,7 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( ColumnPolicyProperty { with: false, - policy_name: "p2".into(), + policy_name: ObjectName::from(vec![Ident::new("p2")]), using_columns: None, } )), @@ -838,8 +883,14 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Tags(TagsColumnOption { with: false, tags: vec![ - Tag::new("C".into(), "TAG C".into()), - Tag::new("D".into(), "TAG D".into()), + Tag::new( + ObjectName::from(vec![Ident::new("C")]), + "TAG C".into() + ), + Tag::new( + ObjectName::from(vec![Ident::new("D")]), + "TAG D".into() + ), ] }), } @@ -855,8 +906,8 @@ fn test_snowflake_create_table_with_several_column_options() { #[test] fn test_snowflake_create_iceberg_table_all_options() { match snowflake().verified_stmt("CREATE ICEBERG TABLE my_table (a INT, b INT) \ - CLUSTER BY (a, b) EXTERNAL_VOLUME = 'volume' CATALOG = 'SNOWFLAKE' BASE_LOCATION = 'relative/path' CATALOG_SYNC = 'OPEN_CATALOG' \ - STORAGE_SERIALIZATION_POLICY = COMPATIBLE COPY GRANTS CHANGE_TRACKING=TRUE DATA_RETENTION_TIME_IN_DAYS=5 MAX_DATA_EXTENSION_TIME_IN_DAYS=10 \ + CLUSTER BY (a, b) EXTERNAL_VOLUME='volume' CATALOG='SNOWFLAKE' BASE_LOCATION='relative/path' CATALOG_SYNC='OPEN_CATALOG' \ + STORAGE_SERIALIZATION_POLICY=COMPATIBLE COPY GRANTS CHANGE_TRACKING=TRUE DATA_RETENTION_TIME_IN_DAYS=5 MAX_DATA_EXTENSION_TIME_IN_DAYS=10 \ WITH AGGREGATION POLICY policy_name WITH ROW ACCESS POLICY policy_name ON (a) WITH TAG (A='TAG A', B='TAG B')") { Statement::CreateTable(CreateTable { name, cluster_by, base_location, @@ -869,8 +920,8 @@ fn test_snowflake_create_iceberg_table_all_options() { assert_eq!("my_table", name.to_string()); assert_eq!( Some(WrappedCollection::Parentheses(vec![ - Ident::new("a"), - Ident::new("b"), + Expr::Identifier(Ident::new("a")), + Expr::Identifier(Ident::new("b")), ])), cluster_by ); @@ -892,8 +943,8 @@ fn test_snowflake_create_iceberg_table_all_options() { with_aggregation_policy.map(|name| name.to_string()) ); assert_eq!(Some(vec![ - Tag::new("A".into(), "TAG A".into()), - Tag::new("B".into(), "TAG B".into()), + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".into()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".into()), ]), with_tags); } @@ -904,7 +955,7 @@ fn test_snowflake_create_iceberg_table_all_options() { #[test] fn test_snowflake_create_iceberg_table() { match snowflake() - .verified_stmt("CREATE ICEBERG TABLE my_table (a INT) BASE_LOCATION = 'relative_path'") + .verified_stmt("CREATE ICEBERG TABLE my_table (a INT) BASE_LOCATION='relative_path'") { Statement::CreateTable(CreateTable { name, @@ -927,6 +978,30 @@ fn test_snowflake_create_iceberg_table_without_location() { ); } +#[test] +fn test_snowflake_create_table_trailing_options() { + // Serialization to SQL assume that in `CREATE TABLE AS` the options come before the `AS ()` + // but Snowflake supports also the other way around + snowflake() + .verified_stmt("CREATE TEMPORARY TABLE dst ON COMMIT PRESERVE ROWS AS (SELECT * FROM src)"); + snowflake() + .parse_sql_statements( + "CREATE TEMPORARY TABLE dst AS (SELECT * FROM src) ON COMMIT PRESERVE ROWS", + ) + .unwrap(); + + // Same for `CREATE TABLE LIKE|CLONE`: + snowflake().verified_stmt("CREATE TEMPORARY TABLE dst LIKE src ON COMMIT PRESERVE ROWS"); + snowflake() + .parse_sql_statements("CREATE TEMPORARY TABLE dst ON COMMIT PRESERVE ROWS LIKE src") + .unwrap(); + + snowflake().verified_stmt("CREATE TEMPORARY TABLE dst CLONE src ON COMMIT PRESERVE ROWS"); + snowflake() + .parse_sql_statements("CREATE TEMPORARY TABLE dst ON COMMIT PRESERVE ROWS CLONE src") + .unwrap(); +} + #[test] fn parse_sf_create_or_replace_view_with_comment_missing_equal() { assert!(snowflake_and_generic() @@ -991,6 +1066,79 @@ fn parse_sf_create_table_or_view_with_dollar_quoted_comment() { ); } +#[test] +fn parse_create_dynamic_table() { + snowflake().verified_stmt(r#"CREATE OR REPLACE DYNAMIC TABLE my_dynamic_table TARGET_LAG='20 minutes' WAREHOUSE=mywh AS SELECT product_id, product_name FROM staging_table"#); + snowflake().verified_stmt(concat!( + "CREATE DYNAMIC ICEBERG TABLE my_dynamic_table (date TIMESTAMP_NTZ, id NUMBER, content STRING)", + " EXTERNAL_VOLUME='my_external_volume'", + " CATALOG='SNOWFLAKE'", + " BASE_LOCATION='my_iceberg_table'", + " TARGET_LAG='20 minutes'", + " WAREHOUSE=mywh", + " AS SELECT product_id, product_name FROM staging_table" + )); + + snowflake().verified_stmt(concat!( + "CREATE DYNAMIC TABLE my_dynamic_table (date TIMESTAMP_NTZ, id NUMBER, content VARIANT)", + " CLUSTER BY (date, id)", + " TARGET_LAG='20 minutes'", + " WAREHOUSE=mywh", + " AS SELECT product_id, product_name FROM staging_table" + )); + + snowflake().verified_stmt(concat!( + "CREATE DYNAMIC TABLE my_cloned_dynamic_table", + " CLONE my_dynamic_table", + " AT(TIMESTAMP => TO_TIMESTAMP_TZ('04/05/2013 01:02:03', 'mm/dd/yyyy hh24:mi:ss'))" + )); + + snowflake().verified_stmt(concat!( + "CREATE DYNAMIC TABLE my_cloned_dynamic_table", + " CLONE my_dynamic_table", + " BEFORE(OFFSET => TO_TIMESTAMP_TZ('04/05/2013 01:02:03', 'mm/dd/yyyy hh24:mi:ss'))" + )); + + snowflake().verified_stmt(concat!( + "CREATE DYNAMIC TABLE my_dynamic_table", + " TARGET_LAG='DOWNSTREAM'", + " WAREHOUSE=mywh", + " INITIALIZE=ON_SCHEDULE", + " REQUIRE USER", + " AS SELECT product_id, product_name FROM staging_table" + )); + + snowflake().verified_stmt(concat!( + "CREATE DYNAMIC TABLE my_dynamic_table", + " TARGET_LAG='DOWNSTREAM'", + " WAREHOUSE=mywh", + " REFRESH_MODE=AUTO", + " INITIALIZE=ON_SCHEDULE", + " REQUIRE USER", + " AS SELECT product_id, product_name FROM staging_table" + )); + + snowflake().verified_stmt(concat!( + "CREATE DYNAMIC TABLE my_dynamic_table", + " TARGET_LAG='DOWNSTREAM'", + " WAREHOUSE=mywh", + " REFRESH_MODE=FULL", + " INITIALIZE=ON_SCHEDULE", + " REQUIRE USER", + " AS SELECT product_id, product_name FROM staging_table" + )); + + snowflake().verified_stmt(concat!( + "CREATE DYNAMIC TABLE my_dynamic_table", + " TARGET_LAG='DOWNSTREAM'", + " WAREHOUSE=mywh", + " REFRESH_MODE=INCREMENTAL", + " INITIALIZE=ON_SCHEDULE", + " REQUIRE USER", + " AS SELECT product_id, product_name FROM staging_table" + )); +} + #[test] fn test_sf_derived_table_in_parenthesis() { // Nesting a subquery in an extra set of parentheses is non-standard, @@ -1578,6 +1726,13 @@ fn test_alter_table_clustering() { snowflake_and_generic().verified_stmt("ALTER TABLE tbl RESUME RECLUSTER"); } +#[test] +fn test_alter_iceberg_table() { + snowflake_and_generic().verified_stmt("ALTER ICEBERG TABLE tbl DROP CLUSTERING KEY"); + snowflake_and_generic().verified_stmt("ALTER ICEBERG TABLE tbl SUSPEND RECLUSTER"); + snowflake_and_generic().verified_stmt("ALTER ICEBERG TABLE tbl RESUME RECLUSTER"); +} + #[test] fn test_drop_stage() { match snowflake_and_generic().verified_stmt("DROP STAGE s1") { @@ -2453,10 +2608,7 @@ fn test_snowflake_stage_object_names_into_location() { .zip(allowed_object_names.iter_mut()) { let (formatted_name, object_name) = it; - let sql = format!( - "COPY INTO {} FROM 'gcs://mybucket/./../a.csv'", - formatted_name - ); + let sql = format!("COPY INTO {formatted_name} FROM 'gcs://mybucket/./../a.csv'"); match snowflake().verified_stmt(&sql) { Statement::CopyIntoSnowflake { into, .. } => { assert_eq!(into.0, object_name.0) @@ -2479,10 +2631,7 @@ fn test_snowflake_stage_object_names_into_table() { .zip(allowed_object_names.iter_mut()) { let (formatted_name, object_name) = it; - let sql = format!( - "COPY INTO {} FROM 'gcs://mybucket/./../a.csv'", - formatted_name - ); + let sql = format!("COPY INTO {formatted_name} FROM 'gcs://mybucket/./../a.csv'"); match snowflake().verified_stmt(&sql) { Statement::CopyIntoSnowflake { into, .. } => { assert_eq!(into.0, object_name.0) @@ -2512,6 +2661,26 @@ fn test_snowflake_copy_into() { } _ => unreachable!(), } + + // Test for non-ident characters in stage names + let sql = "COPY INTO a.b FROM @namespace.stage_name/x@x~x%x+/20250723_data"; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { into, from_obj, .. } => { + assert_eq!( + into, + ObjectName::from(vec![Ident::new("a"), Ident::new("b")]) + ); + assert_eq!( + from_obj, + Some(ObjectName::from(vec![ + Ident::new("@namespace"), + Ident::new("stage_name/x@x~x%x+/20250723_data") + ])) + ) + } + _ => unreachable!(), + } } #[test] @@ -2963,7 +3132,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {}", object_name)), + snowflake().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -2971,7 +3140,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + snowflake().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -2983,7 +3152,9 @@ fn parse_use() { for "e in "e_styles { // Test double identifier with different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), + snowflake().verified_stmt(&format!( + "USE {quote}CATALOG{quote}.{quote}my_schema{quote}" + )), Statement::Use(Use::Object(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") @@ -3002,35 +3173,37 @@ fn parse_use() { for "e in "e_styles { // Test single and double identifier with keyword and different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)), + snowflake().verified_stmt(&format!("USE DATABASE {quote}my_database{quote}")), Statement::Use(Use::Database(ObjectName::from(vec![Ident::with_quote( quote, "my_database".to_string(), )]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)), + snowflake().verified_stmt(&format!("USE SCHEMA {quote}my_schema{quote}")), Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote( quote, "my_schema".to_string(), )]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE SCHEMA {0}CATALOG{0}.{0}my_schema{0}", quote)), + snowflake().verified_stmt(&format!( + "USE SCHEMA {quote}CATALOG{quote}.{quote}my_schema{quote}" + )), Statement::Use(Use::Schema(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") ]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE ROLE {0}my_role{0}", quote)), + snowflake().verified_stmt(&format!("USE ROLE {quote}my_role{quote}")), Statement::Use(Use::Role(ObjectName::from(vec![Ident::with_quote( quote, "my_role".to_string(), )]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE WAREHOUSE {0}my_wh{0}", quote)), + snowflake().verified_stmt(&format!("USE WAREHOUSE {quote}my_wh{quote}")), Statement::Use(Use::Warehouse(ObjectName::from(vec![Ident::with_quote( quote, "my_wh".to_string(), @@ -3067,7 +3240,7 @@ fn view_comment_option_should_be_after_column_list() { "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') COMMENT = 'Comment' AS SELECT a FROM t", "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') WITH (foo = bar) COMMENT = 'Comment' AS SELECT a FROM t", ] { - snowflake_and_generic() + snowflake() .verified_stmt(sql); } } @@ -3076,7 +3249,7 @@ fn view_comment_option_should_be_after_column_list() { fn parse_view_column_descriptions() { let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; - match snowflake_and_generic().verified_stmt(sql) { + match snowflake().verified_stmt(sql) { Statement::CreateView { name, columns, .. } => { assert_eq!(name.to_string(), "v"); assert_eq!( @@ -3085,7 +3258,9 @@ fn parse_view_column_descriptions() { ViewColumnDef { name: Ident::new("a"), data_type: None, - options: Some(vec![ColumnOption::Comment("Comment".to_string())]), + options: Some(ColumnOptions::SpaceSeparated(vec![ColumnOption::Comment( + "Comment".to_string() + )])), }, ViewColumnDef { name: Ident::new("b"), @@ -3340,10 +3515,38 @@ fn parse_ls_and_rm() { .unwrap(); } +#[test] +fn test_sql_keywords_as_select_item_ident() { + // Some keywords that should be parsed as an alias + let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT", "SORT"]; + for kw in unreserved_kws { + snowflake().verified_stmt(&format!("SELECT 1, {kw}")); + } + + // Some keywords that should not be parsed as an alias + let reserved_kws = vec![ + "FROM", + "GROUP", + "HAVING", + "INTERSECT", + "INTO", + "ORDER", + "SELECT", + "UNION", + "WHERE", + "WITH", + ]; + for kw in reserved_kws { + assert!(snowflake() + .parse_sql_statements(&format!("SELECT 1, {kw}")) + .is_err()); + } +} + #[test] fn test_sql_keywords_as_select_item_aliases() { // Some keywords that should be parsed as an alias - let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT"]; + let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT", "SORT"]; for kw in unreserved_kws { snowflake() .one_statement_parses_to(&format!("SELECT 1 {kw}"), &format!("SELECT 1 AS {kw}")); @@ -3367,6 +3570,91 @@ fn test_sql_keywords_as_select_item_aliases() { .parse_sql_statements(&format!("SELECT 1 {kw}")) .is_err()); } + + // LIMIT is alias + snowflake().one_statement_parses_to("SELECT 1 LIMIT", "SELECT 1 AS LIMIT"); + // LIMIT is not an alias + snowflake().verified_stmt("SELECT 1 LIMIT 1"); + snowflake().verified_stmt("SELECT 1 LIMIT $1"); + snowflake().verified_stmt("SELECT 1 LIMIT ''"); + snowflake().verified_stmt("SELECT 1 LIMIT NULL"); + snowflake().verified_stmt("SELECT 1 LIMIT $$$$"); +} + +#[test] +fn test_sql_keywords_as_table_aliases() { + // Some keywords that should be parsed as an alias implicitly + let unreserved_kws = vec![ + "VIEW", + "EXPLAIN", + "ANALYZE", + "SORT", + "PIVOT", + "UNPIVOT", + "TOP", + "LIMIT", + "OFFSET", + "FETCH", + "EXCEPT", + "CLUSTER", + "DISTRIBUTE", + "GLOBAL", + "ANTI", + "SEMI", + "RETURNING", + "OUTER", + "WINDOW", + "END", + "PARTITION", + "PREWHERE", + "SETTINGS", + "FORMAT", + "MATCH_RECOGNIZE", + "OPEN", + ]; + + for kw in unreserved_kws { + snowflake().verified_stmt(&format!("SELECT * FROM tbl AS {kw}")); + snowflake().one_statement_parses_to( + &format!("SELECT * FROM tbl {kw}"), + &format!("SELECT * FROM tbl AS {kw}"), + ); + } + + // Some keywords that should not be parsed as an alias implicitly + let reserved_kws = vec![ + "FROM", "GROUP", "HAVING", "ORDER", "SELECT", "UNION", "WHERE", "WITH", + ]; + for kw in reserved_kws { + assert!(snowflake() + .parse_sql_statements(&format!("SELECT * FROM tbl {kw}")) + .is_err()); + } + + // LIMIT is alias + snowflake().one_statement_parses_to("SELECT * FROM tbl LIMIT", "SELECT * FROM tbl AS LIMIT"); + // LIMIT is not an alias + snowflake().verified_stmt("SELECT * FROM tbl LIMIT 1"); + snowflake().verified_stmt("SELECT * FROM tbl LIMIT $1"); + snowflake().verified_stmt("SELECT * FROM tbl LIMIT ''"); + snowflake().verified_stmt("SELECT * FROM tbl LIMIT NULL"); + snowflake().verified_stmt("SELECT * FROM tbl LIMIT $$$$"); +} + +#[test] +fn test_sql_keywords_as_table_factor() { + // LIMIT is a table factor, Snowflake does not reserve it + snowflake().verified_stmt("SELECT * FROM tbl, LIMIT"); + // LIMIT is not a table factor + snowflake().one_statement_parses_to("SELECT * FROM tbl, LIMIT 1", "SELECT * FROM tbl LIMIT 1"); + + // Table functions are table factors + snowflake().verified_stmt("SELECT 1 FROM TABLE(GENERATOR(ROWCOUNT => 10)) AS a, TABLE(GENERATOR(ROWCOUNT => 10)) AS b"); + + // ORDER is reserved + assert!(snowflake() + .parse_sql_statements("SELECT * FROM tbl, order") + .is_err()); } #[test] @@ -3570,7 +3858,7 @@ fn test_alter_session_followed_by_statement() { .unwrap(); match stmts[..] { [Statement::AlterSession { .. }, Statement::Query { .. }] => {} - _ => panic!("Unexpected statements: {:?}", stmts), + _ => panic!("Unexpected statements: {stmts:?}"), } } @@ -4025,3 +4313,303 @@ fn parse_connect_by_root_operator() { "sql parser error: Expected an expression, found: FROM" ); } + +#[test] +fn test_begin_exception_end() { + for sql in [ + "BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END", + "BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE EX_1; END", + "BEGIN SELECT 1; EXCEPTION WHEN FOO THEN SELECT 2; WHEN OTHER THEN SELECT 3; RAISE; END", + "BEGIN BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END; END", + ] { + snowflake().verified_stmt(sql); + } + + let sql = r#" +DECLARE + EXCEPTION_1 EXCEPTION (-20001, 'I caught the expected exception.'); + EXCEPTION_2 EXCEPTION (-20002, 'Not the expected exception!'); + EXCEPTION_3 EXCEPTION (-20003, 'The worst exception...'); +BEGIN + BEGIN + SELECT 1; + EXCEPTION + WHEN EXCEPTION_1 THEN + SELECT 1; + WHEN EXCEPTION_2 OR EXCEPTION_3 THEN + SELECT 2; + SELECT 3; + WHEN OTHER THEN + SELECT 4; + RAISE; + END; +END +"#; + + // Outer `BEGIN` of the two nested `BEGIN` statements. + let Statement::StartTransaction { mut statements, .. } = snowflake() + .parse_sql_statements(sql) + .unwrap() + .pop() + .unwrap() + else { + unreachable!(); + }; + + // Inner `BEGIN` of the two nested `BEGIN` statements. + let Statement::StartTransaction { + statements, + exception, + has_end_keyword, + .. + } = statements.pop().unwrap() + else { + unreachable!(); + }; + + assert_eq!(1, statements.len()); + assert!(has_end_keyword); + + let exception = exception.unwrap(); + assert_eq!(3, exception.len()); + assert_eq!(1, exception[0].idents.len()); + assert_eq!(1, exception[0].statements.len()); + assert_eq!(2, exception[1].idents.len()); + assert_eq!(2, exception[1].statements.len()); +} + +#[test] +fn test_snowflake_fetch_clause_syntax() { + let canonical = "SELECT c1 FROM fetch_test FETCH FIRST 2 ROWS ONLY"; + snowflake().verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH 2", canonical); + + snowflake() + .verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH FIRST 2", canonical); + snowflake() + .verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH NEXT 2", canonical); + + snowflake() + .verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH 2 ROW", canonical); + + snowflake().verified_only_select_with_canonical( + "SELECT c1 FROM fetch_test FETCH FIRST 2 ROWS", + canonical, + ); +} + +#[test] +fn test_snowflake_create_view_with_multiple_column_options() { + let create_view_with_tag = + r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} + +#[test] +fn test_snowflake_create_view_with_composite_tag() { + let create_view_with_tag = + r#"CREATE VIEW X (COL WITH TAG (foo.bar.baz.pii='email')) AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} + +#[test] +fn test_snowflake_create_view_with_composite_policy_name() { + let create_view_with_tag = + r#"CREATE VIEW X (COL WITH MASKING POLICY foo.bar.baz) AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} + +#[test] +fn test_snowflake_identifier_function() { + // Using IDENTIFIER to reference a column + match &snowflake() + .verified_only_select("SELECT identifier('email') FROM customers") + .projection[0] + { + SelectItem::UnnamedExpr(Expr::Function(Function { name, args, .. })) => { + assert_eq!(*name, ObjectName::from(vec![Ident::new("identifier")])); + assert_eq!( + *args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("email".to_string()).into() + )))], + clauses: vec![], + duplicate_treatment: None + }) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a case-sensitive column + match &snowflake() + .verified_only_select(r#"SELECT identifier('"Email"') FROM customers"#) + .projection[0] + { + SelectItem::UnnamedExpr(Expr::Function(Function { name, args, .. })) => { + assert_eq!(*name, ObjectName::from(vec![Ident::new("identifier")])); + assert_eq!( + *args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("\"Email\"".to_string()).into() + )))], + clauses: vec![], + duplicate_treatment: None + }) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference an alias of a table + match &snowflake() + .verified_only_select("SELECT identifier('alias1').* FROM tbl AS alias1") + .projection[0] + { + SelectItem::QualifiedWildcard( + SelectItemQualifiedWildcardKind::Expr(Expr::Function(Function { name, args, .. })), + _, + ) => { + assert_eq!(*name, ObjectName::from(vec![Ident::new("identifier")])); + assert_eq!( + *args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("alias1".to_string()).into() + )))], + clauses: vec![], + duplicate_treatment: None + }) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a database + match snowflake().verified_stmt("CREATE DATABASE IDENTIFIER('tbl')") { + Statement::CreateDatabase { db_name, .. } => { + assert_eq!( + db_name, + ObjectName(vec![ObjectNamePart::Function(ObjectNamePartFunction { + name: Ident::new("IDENTIFIER"), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("tbl".to_string()).into() + )))] + })]) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a schema + match snowflake().verified_stmt("CREATE SCHEMA IDENTIFIER('db1.sc1')") { + Statement::CreateSchema { schema_name, .. } => { + assert_eq!( + schema_name, + SchemaName::Simple(ObjectName(vec![ObjectNamePart::Function( + ObjectNamePartFunction { + name: Ident::new("IDENTIFIER"), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("db1.sc1".to_string()).into() + )))] + } + )])) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a table + match snowflake().verified_stmt("CREATE TABLE IDENTIFIER('tbl') (id INT)") { + Statement::CreateTable(CreateTable { name, .. }) => { + assert_eq!( + name, + ObjectName(vec![ObjectNamePart::Function(ObjectNamePartFunction { + name: Ident::new("IDENTIFIER"), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("tbl".to_string()).into() + )))] + })]) + ); + } + _ => unreachable!(), + } + + // Cannot have more than one IDENTIFIER part in an object name + assert_eq!( + snowflake() + .parse_sql_statements( + "CREATE TABLE IDENTIFIER('db1').IDENTIFIER('sc1').IDENTIFIER('tbl') (id INT)" + ) + .is_err(), + true + ); + assert_eq!( + snowflake() + .parse_sql_statements("CREATE TABLE IDENTIFIER('db1')..IDENTIFIER('tbl') (id INT)") + .is_err(), + true + ); +} + +#[test] +fn test_create_database() { + snowflake().verified_stmt("CREATE DATABASE my_db"); + snowflake().verified_stmt("CREATE OR REPLACE DATABASE my_db"); + snowflake().verified_stmt("CREATE TRANSIENT DATABASE IF NOT EXISTS my_db"); + snowflake().verified_stmt("CREATE DATABASE my_db CLONE src_db"); + snowflake().verified_stmt( + "CREATE OR REPLACE DATABASE my_db CLONE src_db DATA_RETENTION_TIME_IN_DAYS = 1", + ); + snowflake().one_statement_parses_to( + r#" + CREATE OR REPLACE TRANSIENT DATABASE IF NOT EXISTS my_db + CLONE src_db + DATA_RETENTION_TIME_IN_DAYS = 1 + MAX_DATA_EXTENSION_TIME_IN_DAYS = 5 + EXTERNAL_VOLUME = 'volume1' + CATALOG = 'my_catalog' + REPLACE_INVALID_CHARACTERS = TRUE + DEFAULT_DDL_COLLATION = 'en-ci' + STORAGE_SERIALIZATION_POLICY = COMPATIBLE + COMMENT = 'This is my database' + CATALOG_SYNC = 'sync_integration' + CATALOG_SYNC_NAMESPACE_MODE = NEST + CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '/' + WITH TAG (env = 'prod', team = 'data') + WITH CONTACT (owner = 'admin', dpo = 'compliance') + "#, + "CREATE OR REPLACE TRANSIENT DATABASE IF NOT EXISTS \ + my_db CLONE src_db DATA_RETENTION_TIME_IN_DAYS = 1 MAX_DATA_EXTENSION_TIME_IN_DAYS = 5 \ + EXTERNAL_VOLUME = 'volume1' CATALOG = 'my_catalog' \ + REPLACE_INVALID_CHARACTERS = TRUE DEFAULT_DDL_COLLATION = 'en-ci' \ + STORAGE_SERIALIZATION_POLICY = COMPATIBLE COMMENT = 'This is my database' \ + CATALOG_SYNC = 'sync_integration' CATALOG_SYNC_NAMESPACE_MODE = NEST \ + CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '/' \ + WITH TAG (env='prod', team='data') \ + WITH CONTACT (owner = admin, dpo = compliance)", + ); + + let err = snowflake() + .parse_sql_statements("CREATE DATABASE") + .unwrap_err() + .to_string(); + assert!(err.contains("Expected"), "Unexpected error: {err}"); + + let err = snowflake() + .parse_sql_statements("CREATE DATABASE my_db CLONE") + .unwrap_err() + .to_string(); + assert!(err.contains("Expected"), "Unexpected error: {err}"); +} + +#[test] +fn test_drop_constraints() { + snowflake().verified_stmt("ALTER TABLE tbl DROP PRIMARY KEY"); + snowflake().verified_stmt("ALTER TABLE tbl DROP FOREIGN KEY k1"); + snowflake().verified_stmt("ALTER TABLE tbl DROP CONSTRAINT c1"); + snowflake().verified_stmt("ALTER TABLE tbl DROP PRIMARY KEY CASCADE"); + snowflake().verified_stmt("ALTER TABLE tbl DROP FOREIGN KEY k1 RESTRICT"); + snowflake().verified_stmt("ALTER TABLE tbl DROP CONSTRAINT c1 CASCADE"); +} diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 361c9b051..114aca03a 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -324,7 +324,7 @@ fn parse_create_table_on_conflict_col() { Keyword::IGNORE, Keyword::REPLACE, ] { - let sql = format!("CREATE TABLE t1 (a INT, b INT ON CONFLICT {:?})", keyword); + let sql = format!("CREATE TABLE t1 (a INT, b INT ON CONFLICT {keyword:?})"); match sqlite_and_generic().verified_stmt(&sql) { Statement::CreateTable(CreateTable { columns, .. }) => { assert_eq!( @@ -410,7 +410,7 @@ fn parse_window_function_with_filter() { "count", "user_defined_function", ] { - let sql = format!("SELECT {}(x) FILTER (WHERE y) OVER () FROM t", func_name); + let sql = format!("SELECT {func_name}(x) FILTER (WHERE y) OVER () FROM t"); let select = sqlite().verified_only_select(&sql); assert_eq!(select.to_string(), sql); assert_eq!( @@ -444,7 +444,7 @@ fn parse_window_function_with_filter() { fn parse_attach_database() { let sql = "ATTACH DATABASE 'test.db' AS test"; let verified_stmt = sqlite().verified_stmt(sql); - assert_eq!(sql, format!("{}", verified_stmt)); + assert_eq!(sql, format!("{verified_stmt}")); match verified_stmt { Statement::AttachDatabase { schema_name, @@ -485,7 +485,8 @@ fn parse_update_tuple_row_values() { joins: vec![], }, from: None, - returning: None + returning: None, + limit: None } ); } @@ -562,6 +563,53 @@ fn test_dollar_identifier_as_placeholder() { } } +#[test] +fn test_match_operator() { + assert_eq!( + sqlite().verified_expr("col MATCH 'pattern'"), + Expr::BinaryOp { + op: BinaryOperator::Match, + left: Box::new(Expr::Identifier(Ident::new("col"))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("pattern".to_string())).with_empty_span() + )) + } + ); + sqlite().verified_only_select("SELECT * FROM email WHERE email MATCH 'fts5'"); +} + +#[test] +fn test_regexp_operator() { + assert_eq!( + sqlite().verified_expr("col REGEXP 'pattern'"), + Expr::BinaryOp { + op: BinaryOperator::Regexp, + left: Box::new(Expr::Identifier(Ident::new("col"))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("pattern".to_string())).with_empty_span() + )) + } + ); + sqlite().verified_only_select(r#"SELECT count(*) FROM messages WHERE msg_text REGEXP '\d+'"#); +} + +#[test] +fn test_update_delete_limit() { + match sqlite().verified_stmt("UPDATE foo SET bar = 1 LIMIT 99") { + Statement::Update { limit, .. } => { + assert_eq!(limit, Some(Expr::value(number("99")))); + } + _ => unreachable!(), + } + + match sqlite().verified_stmt("DELETE FROM foo LIMIT 99") { + Statement::Delete(Delete { limit, .. }) => { + assert_eq!(limit, Some(Expr::value(number("99")))); + } + _ => unreachable!(), + } +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) }