diff --git a/.asf.yaml b/.asf.yaml index 8ad476f12..08b31804d 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -36,3 +36,21 @@ github: rebase: false features: issues: true + protected_branches: + main: + required_status_checks: + contexts: + - "codestyle" + - "lint" + - "benchmark-lint" + - "compile" + - "docs" + - "compile-no-std" + - "test (stable)" + - "test (beta)" + - "test (nightly)" + - "Release Audit Tool (RAT)" + pull_requests: + # enable updating head branches of pull requests + allow_update_branch: true + allow_auto_merge: true diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml new file mode 100644 index 000000000..f4524f6b2 --- /dev/null +++ b/.github/workflows/license.yml @@ -0,0 +1,40 @@ +# 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: + merge_group: + +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..2e1da6941 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,10 +17,20 @@ name: Rust -on: [push, pull_request] +on: + push: + # When PR is in the Merge Queue, GitHub will create a temporary branch - but we already have a + # CI running because of `merge_group` + # See also: https://github.com/orgs/community/discussions/15254 + branches-ignore: + - 'gh-readonly-queue/**' + pull_request: + merge_group: -jobs: +permissions: + contents: read +jobs: codestyle: runs-on: ubuntu-latest steps: @@ -85,11 +95,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/CHANGELOG.md b/CHANGELOG.md index ec74bf633..b0c3b5130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,13 +18,14 @@ --> # Changelog -All notable changes to this project will be documented in this file. +All notable changes to this project will be documented in one of the linked +files. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + Given that the parser produces a typed AST, any changes to the AST will technically be breaking and thus will result in a `0.(N+1)` version. - Unreleased: Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. -- `0.52.0`: [changelog/0.52.0.md](changelog/0.52.0.md) -- `0.51.0` and earlier: [changelog/0.51.0-pre.md](changelog/0.51.0-pre.md) +- Past releases: See https://github.com/apache/datafusion-sqlparser-rs/tree/main/changelog diff --git a/Cargo.toml b/Cargo.toml index 8ff0ceb55..ed94bbbdd 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.53.0" +version = "0.59.0" authors = ["Apache DataFusion "] homepage = "https://github.com/apache/datafusion-sqlparser-rs" documentation = "https://docs.rs/sqlparser/" @@ -49,12 +49,12 @@ bigdecimal = { version = "0.4.1", features = ["serde"], optional = true } log = "0.4" recursive = { version = "0.1.1", optional = true} -serde = { version = "1.0", features = ["derive"], optional = true } +serde = { version = "1.0", default-features = false, features = ["derive", "alloc"], optional = true } # serde_json is only used in examples/cli, but we have to put it outside # of dev-dependencies because of # https://github.com/rust-lang/cargo/issues/1596 serde_json = { version = "1.0", optional = true } -sqlparser_derive = { version = "0.3.0", path = "derive", optional = true } +sqlparser_derive = { version = "0.4.0", path = "derive", optional = true } [dev-dependencies] simple_logger = "5.0" diff --git a/README.md b/README.md index 41a44d3d7..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) @@ -53,7 +53,7 @@ println!("AST: {:?}", ast); This outputs ```rust -AST: [Query(Query { ctes: [], body: Select(Select { distinct: false, projection: [UnnamedExpr(Identifier("a")), UnnamedExpr(Identifier("b")), UnnamedExpr(Value(Long(123))), UnnamedExpr(Function(Function { name: ObjectName(["myfunc"]), args: [Identifier("b")], filter: None, over: None, distinct: false }))], from: [TableWithJoins { relation: Table { name: ObjectName(["table_1"]), alias: None, args: [], with_hints: [] }, joins: [] }], selection: Some(BinaryOp { left: BinaryOp { left: Identifier("a"), op: Gt, right: Identifier("b") }, op: And, right: BinaryOp { left: Identifier("b"), op: Lt, right: Value(Long(100)) } }), group_by: [], having: None }), order_by: [OrderByExpr { expr: Identifier("a"), asc: Some(false) }, OrderByExpr { expr: Identifier("b"), asc: None }], limit: None, offset: None, fetch: None })] +AST: [Query(Query { ctes: [], body: Select(Select { distinct: false, projection: [UnnamedExpr(Identifier("a")), UnnamedExpr(Identifier("b")), UnnamedExpr(Value(Long(123))), UnnamedExpr(Function(Function { name:ObjectName([Identifier(Ident { value: "myfunc", quote_style: None })]), args: [Identifier("b")], filter: None, over: None, distinct: false }))], from: [TableWithJoins { relation: Table { name: ObjectName([Identifier(Ident { value: "table_1", quote_style: None })]), alias: None, args: [], with_hints: [] }, joins: [] }], selection: Some(BinaryOp { left: BinaryOp { left: Identifier("a"), op: Gt, right: Identifier("b") }, op: And, right: BinaryOp { left: Identifier("b"), op: Lt, right: Value(Long(100)) } }), group_by: [], having: None }), order_by: [OrderByExpr { expr: Identifier("a"), asc: Some(false) }, OrderByExpr { expr: Identifier("b"), asc: None }], limit: None, offset: None, fetch: None })] ``` @@ -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 @@ -156,7 +160,8 @@ $ cargo run --features json_example --example cli FILENAME.sql [--dialectname] ## Users This parser is currently being used by the [DataFusion] query engine, [LocustDB], -[Ballista], [GlueSQL], [Opteryx], [Polars], [PRQL], [Qrlew], [JumpWire], and [ParadeDB]. +[Ballista], [GlueSQL], [Opteryx], [Polars], [PRQL], [Qrlew], [JumpWire], [ParadeDB], [CipherStash Proxy], +and [GreptimeDB]. If your project is using sqlparser-rs feel free to make a PR to add it to this list. @@ -275,3 +280,5 @@ licensed as above, without any additional terms or conditions. [sql-standard]: https://en.wikipedia.org/wiki/ISO/IEC_9075 [`Dialect`]: https://docs.rs/sqlparser/latest/sqlparser/dialect/trait.Dialect.html [`GenericDialect`]: https://docs.rs/sqlparser/latest/sqlparser/dialect/struct.GenericDialect.html +[CipherStash Proxy]: https://github.com/cipherstash/proxy +[GreptimeDB]: https://github.com/GreptimeTeam/greptimedb diff --git a/changelog/0.54.0.md b/changelog/0.54.0.md new file mode 100644 index 000000000..c0a63ae45 --- /dev/null +++ b/changelog/0.54.0.md @@ -0,0 +1,118 @@ + + +# sqlparser-rs 0.54.0 Changelog + +This release consists of 57 commits from 24 contributors. See credits at the end of this changelog for more information. + +**Implemented enhancements:** + +- feat: support `INSERT INTO [TABLE] FUNCTION` of Clickhouse [#1633](https://github.com/apache/datafusion-sqlparser-rs/pull/1633) (byte-sourcerer) + +**Other:** + +- Run cargo fmt on `derive` crate [#1595](https://github.com/apache/datafusion-sqlparser-rs/pull/1595) (alamb) +- Add Apache license header to spans.rs [#1594](https://github.com/apache/datafusion-sqlparser-rs/pull/1594) (alamb) +- Add support for BigQuery `ANY TYPE` data type [#1602](https://github.com/apache/datafusion-sqlparser-rs/pull/1602) (MartinSahlen) +- Add support for TABLESAMPLE [#1580](https://github.com/apache/datafusion-sqlparser-rs/pull/1580) (yoavcloud) +- Redshift: Fix parsing for quoted numbered columns [#1576](https://github.com/apache/datafusion-sqlparser-rs/pull/1576) (7phs) +- Add the alter table ON COMMIT option to Snowflake [#1606](https://github.com/apache/datafusion-sqlparser-rs/pull/1606) (yoavcloud) +- Support parsing `EXPLAIN ESTIMATE` of Clickhouse [#1605](https://github.com/apache/datafusion-sqlparser-rs/pull/1605) (byte-sourcerer) +- Fix BigQuery hyphenated ObjectName with numbers [#1598](https://github.com/apache/datafusion-sqlparser-rs/pull/1598) (ayman-sigma) +- Fix test compilation issue [#1609](https://github.com/apache/datafusion-sqlparser-rs/pull/1609) (iffyio) +- Allow foreign table constraint without columns [#1608](https://github.com/apache/datafusion-sqlparser-rs/pull/1608) (ramnivas) +- Support optional table for `ANALYZE` statement [#1599](https://github.com/apache/datafusion-sqlparser-rs/pull/1599) (yuyang-ok) +- Support DOUBLE data types with precision for Mysql [#1611](https://github.com/apache/datafusion-sqlparser-rs/pull/1611) (artorias1024) +- Add `#[recursive]` [#1522](https://github.com/apache/datafusion-sqlparser-rs/pull/1522) (blaginin) +- Support arbitrary composite access expressions [#1600](https://github.com/apache/datafusion-sqlparser-rs/pull/1600) (ayman-sigma) +- Consolidate `MapAccess`, and `Subscript` into `CompoundExpr` to handle the complex field access chain [#1551](https://github.com/apache/datafusion-sqlparser-rs/pull/1551) (goldmedal) +- Handle empty projection in Postgres SELECT statements [#1613](https://github.com/apache/datafusion-sqlparser-rs/pull/1613) (tobyhede) +- Merge composite and compound expr test cases [#1615](https://github.com/apache/datafusion-sqlparser-rs/pull/1615) (iffyio) +- Support Snowflake Update-From-Select [#1604](https://github.com/apache/datafusion-sqlparser-rs/pull/1604) (yuval-illumex) +- Improve parsing performance by reducing token cloning [#1587](https://github.com/apache/datafusion-sqlparser-rs/pull/1587) (davisp) +- Improve Parser documentation [#1617](https://github.com/apache/datafusion-sqlparser-rs/pull/1617) (alamb) +- Add support for DROP EXTENSION [#1610](https://github.com/apache/datafusion-sqlparser-rs/pull/1610) (ramnivas) +- Refactor advancing token to avoid duplication, avoid borrow checker issues [#1618](https://github.com/apache/datafusion-sqlparser-rs/pull/1618) (alamb) +- Fix the parsing result for the special double number [#1621](https://github.com/apache/datafusion-sqlparser-rs/pull/1621) (goldmedal) +- SQLite: Allow dollar signs in placeholder names [#1620](https://github.com/apache/datafusion-sqlparser-rs/pull/1620) (hansott) +- Improve error for an unexpected token after DROP [#1623](https://github.com/apache/datafusion-sqlparser-rs/pull/1623) (ramnivas) +- Fix `sqlparser_bench` benchmark compilation [#1625](https://github.com/apache/datafusion-sqlparser-rs/pull/1625) (alamb) +- Improve parsing speed by avoiding some clones in parse_identifier [#1624](https://github.com/apache/datafusion-sqlparser-rs/pull/1624) (alamb) +- Simplify `parse_keyword_apis` more [#1626](https://github.com/apache/datafusion-sqlparser-rs/pull/1626) (alamb) +- Test benchmarks and Improve benchmark README.md [#1627](https://github.com/apache/datafusion-sqlparser-rs/pull/1627) (alamb) +- Add support for MYSQL's `RENAME TABLE` [#1616](https://github.com/apache/datafusion-sqlparser-rs/pull/1616) (wugeer) +- Correctly tokenize nested comments [#1629](https://github.com/apache/datafusion-sqlparser-rs/pull/1629) (hansott) +- Add support for USE SECONDARY ROLE (vs. ROLES) [#1637](https://github.com/apache/datafusion-sqlparser-rs/pull/1637) (yoavcloud) +- Add support for various Snowflake grantees [#1640](https://github.com/apache/datafusion-sqlparser-rs/pull/1640) (yoavcloud) +- Add support for the SQL OVERLAPS predicate [#1638](https://github.com/apache/datafusion-sqlparser-rs/pull/1638) (yoavcloud) +- Add support for Snowflake LIST and REMOVE [#1639](https://github.com/apache/datafusion-sqlparser-rs/pull/1639) (yoavcloud) +- Add support for MySQL's INSERT INTO ... SET syntax [#1641](https://github.com/apache/datafusion-sqlparser-rs/pull/1641) (yoavcloud) +- Start new line if \r in Postgres dialect [#1647](https://github.com/apache/datafusion-sqlparser-rs/pull/1647) (hansott) +- Support pluralized time units [#1630](https://github.com/apache/datafusion-sqlparser-rs/pull/1630) (wugeer) +- Replace `ReferentialAction` enum in `DROP` statements [#1648](https://github.com/apache/datafusion-sqlparser-rs/pull/1648) (stepancheg) +- Add support for MS-SQL BEGIN/END TRY/CATCH [#1649](https://github.com/apache/datafusion-sqlparser-rs/pull/1649) (yoavcloud) +- Fix MySQL parsing of GRANT, REVOKE, and CREATE VIEW [#1538](https://github.com/apache/datafusion-sqlparser-rs/pull/1538) (mvzink) +- Add support for the Snowflake MINUS set operator [#1652](https://github.com/apache/datafusion-sqlparser-rs/pull/1652) (yoavcloud) +- ALTER TABLE DROP {COLUMN|CONSTRAINT} RESTRICT [#1651](https://github.com/apache/datafusion-sqlparser-rs/pull/1651) (stepancheg) +- Add support for ClickHouse `FORMAT` on `INSERT` [#1628](https://github.com/apache/datafusion-sqlparser-rs/pull/1628) (bombsimon) +- MsSQL SET for session params [#1646](https://github.com/apache/datafusion-sqlparser-rs/pull/1646) (yoavcloud) +- Correctly look for end delimiter dollar quoted string [#1650](https://github.com/apache/datafusion-sqlparser-rs/pull/1650) (hansott) +- Support single line comments starting with '#' for Hive [#1654](https://github.com/apache/datafusion-sqlparser-rs/pull/1654) (wugeer) +- Support trailing commas in `FROM` clause [#1645](https://github.com/apache/datafusion-sqlparser-rs/pull/1645) (barsela1) +- Allow empty options for BigQuery [#1657](https://github.com/apache/datafusion-sqlparser-rs/pull/1657) (MartinSahlen) +- Add support for parsing RAISERROR [#1656](https://github.com/apache/datafusion-sqlparser-rs/pull/1656) (AvivDavid-Satori) +- Add support for Snowflake column aliases that use SQL keywords [#1632](https://github.com/apache/datafusion-sqlparser-rs/pull/1632) (yoavcloud) +- fix parsing of `INSERT INTO ... SELECT ... RETURNING ` [#1661](https://github.com/apache/datafusion-sqlparser-rs/pull/1661) (lovasoa) +- Add support for `IS [NOT] [form] NORMALIZED` [#1655](https://github.com/apache/datafusion-sqlparser-rs/pull/1655) (alexander-beedie) +- Add support for qualified column names in JOIN ... USING [#1663](https://github.com/apache/datafusion-sqlparser-rs/pull/1663) (yoavcloud) +- Add support for Snowflake AT/BEFORE [#1667](https://github.com/apache/datafusion-sqlparser-rs/pull/1667) (yoavcloud) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 13 Yoav Cohen + 9 Andrew Lamb + 4 Hans Ott + 3 Ramnivas Laddad + 3 wugeer + 2 Ayman Elkfrawy + 2 Ifeanyi Ubah + 2 Jax Liu + 2 Martin Abelson Sahlen + 2 Stepan Koltsov + 2 cjw + 1 Aleksei Piianin + 1 Alexander Beedie + 1 AvivDavid-Satori + 1 Dmitrii Blaginin + 1 Michael Victor Zink + 1 Ophir LOJKINE + 1 Paul J. Davis + 1 Simon Sawert + 1 Toby Hede + 1 Yuval Shkolar + 1 artorias1024 + 1 bar sela + 1 yuyang +``` + +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.55.0.md b/changelog/0.55.0.md new file mode 100644 index 000000000..046bf22bc --- /dev/null +++ b/changelog/0.55.0.md @@ -0,0 +1,173 @@ + + +# sqlparser-rs 0.55.0 Changelog + +This release consists of 55 commits from 25 contributors. See credits at the end of this changelog for more information. + +## Migrating usages of `Expr::Value` + +In v0.55 of sqlparser the `Expr::Value` enum variant contains a `ValueWithSpan` instead of a `Value`. Here is how to migrate. + +### When pattern matching + +```diff +- Expr::Value(Value::SingleQuotedString(my_string)) => { ... } ++ Expr::Value(ValueWithSpan{ value: Value::SingleQuotedString(my_string), span: _ }) => { ... } +``` + +### When creating an `Expr` + +Use the new `Expr::value` method (notice the lowercase `v`), which will create a `ValueWithSpan` containing an empty span: + +```diff +- Expr::Value(Value::SingleQuotedString(my_string)) ++ Expr::value(Value::SingleQuotedString(my_string)) +``` + +## Migrating usages of `ObjectName` + +In v0.55 of sqlparser, the `ObjectName` structure has been changed as shown below. Here is now to migrate. + +```diff +- pub struct ObjectName(pub Vec); ++ pub struct ObjectName(pub Vec) +``` + +### When constructing `ObjectName` + +Use the `From` impl: + +```diff +- name: ObjectName(vec![Ident::new("f")]), ++ name: ObjectName::from(vec![Ident::new("f")]), +``` + +### Accessing Spans + +Use the `span()` function + +```diff +- name.span ++ name.span() +``` + + + +**Breaking changes:** + +- Enhance object name path segments [#1539](https://github.com/apache/datafusion-sqlparser-rs/pull/1539) (ayman-sigma) +- Store spans for Value expressions [#1738](https://github.com/apache/datafusion-sqlparser-rs/pull/1738) (lovasoa) + +**Implemented enhancements:** + +- feat: adjust create and drop trigger for mysql dialect [#1734](https://github.com/apache/datafusion-sqlparser-rs/pull/1734) (invm) + +**Fixed bugs:** + +- fix: make `serde` feature no_std [#1730](https://github.com/apache/datafusion-sqlparser-rs/pull/1730) (iajoiner) + +**Other:** + +- Update rat_exclude_file.txt [#1670](https://github.com/apache/datafusion-sqlparser-rs/pull/1670) (alamb) +- Add support for Snowflake account privileges [#1666](https://github.com/apache/datafusion-sqlparser-rs/pull/1666) (yoavcloud) +- Add support for Create Iceberg Table statement for Snowflake parser [#1664](https://github.com/apache/datafusion-sqlparser-rs/pull/1664) (Vedin) +- National strings: check if dialect supports backslash escape [#1672](https://github.com/apache/datafusion-sqlparser-rs/pull/1672) (hansott) +- Only support escape literals for Postgres, Redshift and generic dialect [#1674](https://github.com/apache/datafusion-sqlparser-rs/pull/1674) (hansott) +- BigQuery: Support trailing commas in column definitions list [#1682](https://github.com/apache/datafusion-sqlparser-rs/pull/1682) (iffyio) +- Enable GROUP BY exp for Snowflake dialect [#1683](https://github.com/apache/datafusion-sqlparser-rs/pull/1683) (yoavcloud) +- Add support for parsing empty dictionary expressions [#1684](https://github.com/apache/datafusion-sqlparser-rs/pull/1684) (yoavcloud) +- Support multiple tables in `UPDATE FROM` clause [#1681](https://github.com/apache/datafusion-sqlparser-rs/pull/1681) (iffyio) +- Add support for mysql table hints [#1675](https://github.com/apache/datafusion-sqlparser-rs/pull/1675) (AvivDavid-Satori) +- BigQuery: Add support for select expr star [#1680](https://github.com/apache/datafusion-sqlparser-rs/pull/1680) (iffyio) +- Support underscore separators in numbers for Clickhouse. Fixes #1659 [#1677](https://github.com/apache/datafusion-sqlparser-rs/pull/1677) (graup) +- BigQuery: Fix column identifier reserved keywords list [#1678](https://github.com/apache/datafusion-sqlparser-rs/pull/1678) (iffyio) +- Fix bug when parsing a Snowflake stage with `;` suffix [#1688](https://github.com/apache/datafusion-sqlparser-rs/pull/1688) (yoavcloud) +- Allow plain JOIN without turning it into INNER [#1692](https://github.com/apache/datafusion-sqlparser-rs/pull/1692) (mvzink) +- Fix DDL generation in case of an empty arguments function. [#1690](https://github.com/apache/datafusion-sqlparser-rs/pull/1690) (remysaissy) +- Fix `CREATE FUNCTION` round trip for Hive dialect [#1693](https://github.com/apache/datafusion-sqlparser-rs/pull/1693) (iffyio) +- Make numeric literal underscore test dialect agnostic [#1685](https://github.com/apache/datafusion-sqlparser-rs/pull/1685) (iffyio) +- Extend lambda support for ClickHouse and DuckDB dialects [#1686](https://github.com/apache/datafusion-sqlparser-rs/pull/1686) (gstvg) +- Make TypedString preserve quote style [#1679](https://github.com/apache/datafusion-sqlparser-rs/pull/1679) (graup) +- Do not parse ASOF and MATCH_CONDITION as table factor aliases [#1698](https://github.com/apache/datafusion-sqlparser-rs/pull/1698) (yoavcloud) +- Add support for GRANT on some common Snowflake objects [#1699](https://github.com/apache/datafusion-sqlparser-rs/pull/1699) (yoavcloud) +- Add RETURNS TABLE() support for CREATE FUNCTION in Postgresql [#1687](https://github.com/apache/datafusion-sqlparser-rs/pull/1687) (remysaissy) +- Add parsing for GRANT ROLE and GRANT DATABASE ROLE in Snowflake dialect [#1689](https://github.com/apache/datafusion-sqlparser-rs/pull/1689) (yoavcloud) +- Add support for `CREATE/ALTER/DROP CONNECTOR` syntax [#1701](https://github.com/apache/datafusion-sqlparser-rs/pull/1701) (wugeer) +- Parse Snowflake COPY INTO [#1669](https://github.com/apache/datafusion-sqlparser-rs/pull/1669) (yoavcloud) +- Require space after -- to start single line comment in MySQL [#1705](https://github.com/apache/datafusion-sqlparser-rs/pull/1705) (hansott) +- Add suppport for Show Objects statement for the Snowflake parser [#1702](https://github.com/apache/datafusion-sqlparser-rs/pull/1702) (DanCodedThis) +- Fix incorrect parsing of JsonAccess bracket notation after cast in Snowflake [#1708](https://github.com/apache/datafusion-sqlparser-rs/pull/1708) (yoavcloud) +- Parse Postgres VARBIT datatype [#1703](https://github.com/apache/datafusion-sqlparser-rs/pull/1703) (mvzink) +- Implement FROM-first selects [#1713](https://github.com/apache/datafusion-sqlparser-rs/pull/1713) (mitsuhiko) +- Enable custom dialects to support `MATCH() AGAINST()` [#1719](https://github.com/apache/datafusion-sqlparser-rs/pull/1719) (joocer) +- Support group by cube/rollup etc in BigQuery [#1720](https://github.com/apache/datafusion-sqlparser-rs/pull/1720) (Groennbeck) +- Add support for MS Varbinary(MAX) (#1714) [#1715](https://github.com/apache/datafusion-sqlparser-rs/pull/1715) (TylerBrinks) +- Add supports for Hive's `SELECT ... GROUP BY .. GROUPING SETS` syntax [#1653](https://github.com/apache/datafusion-sqlparser-rs/pull/1653) (wugeer) +- Differentiate LEFT JOIN from LEFT OUTER JOIN [#1726](https://github.com/apache/datafusion-sqlparser-rs/pull/1726) (mvzink) +- Add support for Postgres `ALTER TYPE` [#1727](https://github.com/apache/datafusion-sqlparser-rs/pull/1727) (jvatic) +- Replace `Method` and `CompositeAccess` with `CompoundFieldAccess` [#1716](https://github.com/apache/datafusion-sqlparser-rs/pull/1716) (iffyio) +- Add support for `EXECUTE IMMEDIATE` [#1717](https://github.com/apache/datafusion-sqlparser-rs/pull/1717) (iffyio) +- Treat COLLATE like any other column option [#1731](https://github.com/apache/datafusion-sqlparser-rs/pull/1731) (mvzink) +- Add support for PostgreSQL/Redshift geometric operators [#1723](https://github.com/apache/datafusion-sqlparser-rs/pull/1723) (benrsatori) +- Implement SnowFlake ALTER SESSION [#1712](https://github.com/apache/datafusion-sqlparser-rs/pull/1712) (osipovartem) +- Extend Visitor trait for Value type [#1725](https://github.com/apache/datafusion-sqlparser-rs/pull/1725) (tomershaniii) +- Add support for `ORDER BY ALL` [#1724](https://github.com/apache/datafusion-sqlparser-rs/pull/1724) (PokIsemaine) +- Parse casting to array using double colon operator in Redshift [#1737](https://github.com/apache/datafusion-sqlparser-rs/pull/1737) (yoavcloud) +- Replace parallel condition/result vectors with single CaseWhen vector in Expr::Case. This fixes the iteration order when using the `Visitor` trait. Expressions are now visited in the same order as they appear in the sql source. [#1733](https://github.com/apache/datafusion-sqlparser-rs/pull/1733) (lovasoa) +- BigQuery: Add support for `BEGIN` [#1718](https://github.com/apache/datafusion-sqlparser-rs/pull/1718) (iffyio) +- Parse SIGNED INTEGER type in MySQL CAST [#1739](https://github.com/apache/datafusion-sqlparser-rs/pull/1739) (mvzink) +- Parse MySQL ALTER TABLE ALGORITHM option [#1745](https://github.com/apache/datafusion-sqlparser-rs/pull/1745) (mvzink) +- Random test cleanups use Expr::value [#1749](https://github.com/apache/datafusion-sqlparser-rs/pull/1749) (alamb) +- Parse ALTER TABLE AUTO_INCREMENT operation for MySQL [#1748](https://github.com/apache/datafusion-sqlparser-rs/pull/1748) (mvzink) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 10 Yoav Cohen + 9 Ifeanyi Ubah + 7 Michael Victor Zink + 3 Hans Ott + 2 Andrew Lamb + 2 Ophir LOJKINE + 2 Paul Grau + 2 Rémy SAISSY + 2 wugeer + 1 Armin Ronacher + 1 Artem Osipov + 1 AvivDavid-Satori + 1 Ayman Elkfrawy + 1 DanCodedThis + 1 Denys Tsomenko + 1 Emil + 1 Ian Alexander Joiner + 1 Jesse Stuart + 1 Justin Joyce + 1 Michael + 1 SiLe Zhou + 1 Tyler Brinks + 1 benrsatori + 1 gstvg + 1 tomershaniii +``` + +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.56.0.md b/changelog/0.56.0.md new file mode 100644 index 000000000..b3c8a67aa --- /dev/null +++ b/changelog/0.56.0.md @@ -0,0 +1,102 @@ + + +# sqlparser-rs 0.56.0 Changelog + +This release consists of 48 commits from 19 contributors. See credits at the end of this changelog for more information. + +**Other:** + +- Ignore escaped LIKE wildcards in MySQL [#1735](https://github.com/apache/datafusion-sqlparser-rs/pull/1735) (mvzink) +- Parse SET NAMES syntax in Postgres [#1752](https://github.com/apache/datafusion-sqlparser-rs/pull/1752) (mvzink) +- re-add support for nested comments in mssql [#1754](https://github.com/apache/datafusion-sqlparser-rs/pull/1754) (lovasoa) +- Extend support for INDEX parsing [#1707](https://github.com/apache/datafusion-sqlparser-rs/pull/1707) (LucaCappelletti94) +- Parse MySQL `ALTER TABLE DROP FOREIGN KEY` syntax [#1762](https://github.com/apache/datafusion-sqlparser-rs/pull/1762) (mvzink) +- add support for `with` clauses (CTEs) in `delete` statements [#1764](https://github.com/apache/datafusion-sqlparser-rs/pull/1764) (lovasoa) +- SET with a list of comma separated assignments [#1757](https://github.com/apache/datafusion-sqlparser-rs/pull/1757) (MohamedAbdeen21) +- Preserve MySQL-style `LIMIT , ` syntax [#1765](https://github.com/apache/datafusion-sqlparser-rs/pull/1765) (mvzink) +- Add support for `DROP MATERIALIZED VIEW` [#1743](https://github.com/apache/datafusion-sqlparser-rs/pull/1743) (iffyio) +- Add `CASE` and `IF` statement support [#1741](https://github.com/apache/datafusion-sqlparser-rs/pull/1741) (iffyio) +- BigQuery: Add support for `CREATE SCHEMA` options [#1742](https://github.com/apache/datafusion-sqlparser-rs/pull/1742) (iffyio) +- Snowflake: Support dollar quoted comments [#1755](https://github.com/apache/datafusion-sqlparser-rs/pull/1755) +- Add LOCK operation for ALTER TABLE [#1768](https://github.com/apache/datafusion-sqlparser-rs/pull/1768) (MohamedAbdeen21) +- Add support for `RAISE` statement [#1766](https://github.com/apache/datafusion-sqlparser-rs/pull/1766) (iffyio) +- Add GLOBAL context/modifier to SET statements [#1767](https://github.com/apache/datafusion-sqlparser-rs/pull/1767) (MohamedAbdeen21) +- Parse `SUBSTR` as alias for `SUBSTRING` [#1769](https://github.com/apache/datafusion-sqlparser-rs/pull/1769) (mvzink) +- SET statements: scope modifier for multiple assignments [#1772](https://github.com/apache/datafusion-sqlparser-rs/pull/1772) (MohamedAbdeen21) +- Support qualified column names in `MATCH AGAINST` clause [#1774](https://github.com/apache/datafusion-sqlparser-rs/pull/1774) (tomershaniii) +- Mysql: Add support for := operator [#1779](https://github.com/apache/datafusion-sqlparser-rs/pull/1779) (barsela1) +- Add cipherstash-proxy to list of users in README.md [#1782](https://github.com/apache/datafusion-sqlparser-rs/pull/1782) (coderdan) +- Fix typos [#1785](https://github.com/apache/datafusion-sqlparser-rs/pull/1785) (jayvdb) +- Add support for Databricks TIMESTAMP_NTZ. [#1781](https://github.com/apache/datafusion-sqlparser-rs/pull/1781) (romanb) +- Enable double-dot-notation for mssql. [#1787](https://github.com/apache/datafusion-sqlparser-rs/pull/1787) (romanb) +- Fix: Snowflake ALTER SESSION cannot be followed by other statements. [#1786](https://github.com/apache/datafusion-sqlparser-rs/pull/1786) (romanb) +- Add GreptimeDB to the "Users" in README [#1788](https://github.com/apache/datafusion-sqlparser-rs/pull/1788) (MichaelScofield) +- Extend snowflake grant options support [#1794](https://github.com/apache/datafusion-sqlparser-rs/pull/1794) (yoavcloud) +- Fix clippy lint on rust 1.86 [#1796](https://github.com/apache/datafusion-sqlparser-rs/pull/1796) (iffyio) +- Allow single quotes in EXTRACT() for Redshift. [#1795](https://github.com/apache/datafusion-sqlparser-rs/pull/1795) (romanb) +- MSSQL: Add support for functionality `MERGE` output clause [#1790](https://github.com/apache/datafusion-sqlparser-rs/pull/1790) (dilovancelik) +- Support additional DuckDB integer types such as HUGEINT, UHUGEINT, etc [#1797](https://github.com/apache/datafusion-sqlparser-rs/pull/1797) (alexander-beedie) +- Add support for MSSQL IF/ELSE statements. [#1791](https://github.com/apache/datafusion-sqlparser-rs/pull/1791) (romanb) +- Allow literal backslash escapes for string literals in Redshift dialect. [#1801](https://github.com/apache/datafusion-sqlparser-rs/pull/1801) (romanb) +- Add support for MySQL's STRAIGHT_JOIN join operator. [#1802](https://github.com/apache/datafusion-sqlparser-rs/pull/1802) (romanb) +- Snowflake COPY INTO target columns, select items and optional alias [#1805](https://github.com/apache/datafusion-sqlparser-rs/pull/1805) (yoavcloud) +- Fix tokenization of qualified identifiers with numeric prefix. [#1803](https://github.com/apache/datafusion-sqlparser-rs/pull/1803) (romanb) +- Add support for `INHERITS` option in `CREATE TABLE` statement [#1806](https://github.com/apache/datafusion-sqlparser-rs/pull/1806) (LucaCappelletti94) +- Add `DROP TRIGGER` support for SQL Server [#1813](https://github.com/apache/datafusion-sqlparser-rs/pull/1813) (aharpervc) +- Snowflake: support nested join without parentheses [#1799](https://github.com/apache/datafusion-sqlparser-rs/pull/1799) (barsela1) +- Add support for parenthesized subquery as `IN` predicate [#1793](https://github.com/apache/datafusion-sqlparser-rs/pull/1793) (adamchainz) +- Fix `STRAIGHT_JOIN` constraint when table alias is absent [#1812](https://github.com/apache/datafusion-sqlparser-rs/pull/1812) (killertux) +- Add support for `PRINT` statement for SQL Server [#1811](https://github.com/apache/datafusion-sqlparser-rs/pull/1811) (aharpervc) +- enable `supports_filter_during_aggregation` for Generic dialect [#1815](https://github.com/apache/datafusion-sqlparser-rs/pull/1815) (goldmedal) +- Add support for `XMLTABLE` [#1817](https://github.com/apache/datafusion-sqlparser-rs/pull/1817) (lovasoa) +- Add `CREATE FUNCTION` support for SQL Server [#1808](https://github.com/apache/datafusion-sqlparser-rs/pull/1808) (aharpervc) +- Add `OR ALTER` support for `CREATE VIEW` [#1818](https://github.com/apache/datafusion-sqlparser-rs/pull/1818) (aharpervc) +- Add `DECLARE ... CURSOR FOR` support for SQL Server [#1821](https://github.com/apache/datafusion-sqlparser-rs/pull/1821) (aharpervc) +- Handle missing login in changelog generate script [#1823](https://github.com/apache/datafusion-sqlparser-rs/pull/1823) (iffyio) +- Snowflake: Add support for `CONNECT_BY_ROOT` [#1780](https://github.com/apache/datafusion-sqlparser-rs/pull/1780) (tomershaniii) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 8 Roman Borschel + 6 Ifeanyi Ubah + 5 Andrew Harper + 5 Michael Victor Zink + 4 Mohamed Abdeen + 3 Ophir LOJKINE + 2 Luca Cappelletti + 2 Yoav Cohen + 2 bar sela + 2 tomershaniii + 1 Adam Johnson + 1 Aleksei Piianin + 1 Alexander Beedie + 1 Bruno Clemente + 1 Dan Draper + 1 DilovanCelik + 1 Jax Liu + 1 John Vandenberg + 1 LFC +``` + +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.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/changelog/0.59.0.md b/changelog/0.59.0.md new file mode 100644 index 000000000..a3b14f1d8 --- /dev/null +++ b/changelog/0.59.0.md @@ -0,0 +1,122 @@ + + +# sqlparser-rs 0.59.0 Changelog + +This release consists of 59 commits from 22 contributors. See credits at the end of this changelog for more information. + +**Implemented enhancements:** + +- feat: support export data for bigquery [#1976](https://github.com/apache/datafusion-sqlparser-rs/pull/1976) (chenkovsky) +- feat: support multi value columns and aliases in unpivot [#1969](https://github.com/apache/datafusion-sqlparser-rs/pull/1969) (chenkovsky) +- feat: Include end token in `ALTER TABLE` statement [#1999](https://github.com/apache/datafusion-sqlparser-rs/pull/1999) (IndexSeek) +- feat: support multiple value for pivot [#1970](https://github.com/apache/datafusion-sqlparser-rs/pull/1970) (chenkovsky) +- feat: Add `ALTER SCHEMA` support [#1980](https://github.com/apache/datafusion-sqlparser-rs/pull/1980) (chenkovsky) +- feat: MERGE statements: add RETURNING and OUTPUT without INTO [#2011](https://github.com/apache/datafusion-sqlparser-rs/pull/2011) (lovasoa) +- feat: support postgres alter schema [#2038](https://github.com/apache/datafusion-sqlparser-rs/pull/2038) (chenkovsky) + +**Fixed bugs:** + +- fix: begin statement for bigquery [#1975](https://github.com/apache/datafusion-sqlparser-rs/pull/1975) (chenkovsky) +- fix: update DuckDB and ClickHouse documentation links [#1978](https://github.com/apache/datafusion-sqlparser-rs/pull/1978) (IndexSeek) + +**Other:** + +- MySQL: Support `EXPLAIN ANALYZE` format variants [#1945](https://github.com/apache/datafusion-sqlparser-rs/pull/1945) (yoavcloud) +- Add support for `NOT NULL` and `NOTNULL` expressions [#1927](https://github.com/apache/datafusion-sqlparser-rs/pull/1927) (ryanschneider) +- Snowflake: Support `CLONE` option in `CREATE DATABASE/SCHEMA` statements [#1958](https://github.com/apache/datafusion-sqlparser-rs/pull/1958) (yoavcloud) +- Add support for `GRANT DROP` statement [#1959](https://github.com/apache/datafusion-sqlparser-rs/pull/1959) (yoavcloud) +- Snowflake: Add support for `CREATE USER` [#1950](https://github.com/apache/datafusion-sqlparser-rs/pull/1950) (yoavcloud) +- Postgres: Support parenthesized `SET` options for `ALTER TABLE` [#1947](https://github.com/apache/datafusion-sqlparser-rs/pull/1947) (achristmascarl) +- Snowflake: Support IDENTIFIER for GRANT ROLE [#1957](https://github.com/apache/datafusion-sqlparser-rs/pull/1957) (yoavcloud) +- Snowflake: Numeric prefix for stage name part [#1966](https://github.com/apache/datafusion-sqlparser-rs/pull/1966) (yoavcloud) +- Snowflake: Support `GRANT CREATE SCHEMA` `GRANT .. ON ALL FUNCTIONS IN SCHEMA` [#1964](https://github.com/apache/datafusion-sqlparser-rs/pull/1964) (yoavcloud) +- Snowflake: DROP STREAM [#1973](https://github.com/apache/datafusion-sqlparser-rs/pull/1973) (yoavcloud) +- Add ODBC escape syntax support for time expressions [#1953](https://github.com/apache/datafusion-sqlparser-rs/pull/1953) (etgarperets) +- Add support for `SHOW CHARSET` [#1974](https://github.com/apache/datafusion-sqlparser-rs/pull/1974) (etgarperets) +- Snowflake: Support `CREATE VIEW myview IF NOT EXISTS` [#1961](https://github.com/apache/datafusion-sqlparser-rs/pull/1961) (etgarperets) +- Update criterion requirement from 0.6 to 0.7 in /sqlparser_bench [#1981](https://github.com/apache/datafusion-sqlparser-rs/pull/1981) (dependabot[bot]) +- Snowflake: Improve support for reserved keywords for table factor [#1942](https://github.com/apache/datafusion-sqlparser-rs/pull/1942) (yoavcloud) +- MySQL: Allow optional `SIGNED` suffix on integer data types [#1985](https://github.com/apache/datafusion-sqlparser-rs/pull/1985) (mvzink) +- Fix placeholder spans [#1979](https://github.com/apache/datafusion-sqlparser-rs/pull/1979) (xitep) +- Snowflake create database [#1939](https://github.com/apache/datafusion-sqlparser-rs/pull/1939) (osipovartem) +- Postgres: Support `INTERVAL` data type options [#1984](https://github.com/apache/datafusion-sqlparser-rs/pull/1984) (mvzink) +- MySQL: Support comma-separated `CREATE TABLE` options [#1989](https://github.com/apache/datafusion-sqlparser-rs/pull/1989) (mvzink) +- MySQL: Support `ALTER TABLE RENAME AS` [#1965](https://github.com/apache/datafusion-sqlparser-rs/pull/1965) (altmannmarcelo) +- Improve MySQL `CREATE TRIGGER` parsing [#1998](https://github.com/apache/datafusion-sqlparser-rs/pull/1998) (mvzink) +- Snowflake - support table function in table factor (regression) [#1996](https://github.com/apache/datafusion-sqlparser-rs/pull/1996) (tomershaniii) +- Improve MySQL option parsing in index definitions [#1997](https://github.com/apache/datafusion-sqlparser-rs/pull/1997) (mvzink) +- Add support for `UPDATE ... LIMIT ...` [#1991](https://github.com/apache/datafusion-sqlparser-rs/pull/1991) (xtuc) +- Postgres: enhance NUMERIC/DECIMAL parsing to support negative scale [#1990](https://github.com/apache/datafusion-sqlparser-rs/pull/1990) (IndexSeek) +- Fix column definition `COLLATE` parsing [#1986](https://github.com/apache/datafusion-sqlparser-rs/pull/1986) (mvzink) +- Redshift: CREATE TABLE ... (LIKE ..) [#1967](https://github.com/apache/datafusion-sqlparser-rs/pull/1967) (yoavcloud) +- Add drop behavior to `DROP PRIMARY/FOREIGN KEY` [#2002](https://github.com/apache/datafusion-sqlparser-rs/pull/2002) (yoavcloud) +- Redshift: Add support for IAM_ROLE and IGNOREHEADER COPY options [#1968](https://github.com/apache/datafusion-sqlparser-rs/pull/1968) (yoavcloud) +- Snowflake: Add support for `CREATE DYNAMIC TABLE` [#1960](https://github.com/apache/datafusion-sqlparser-rs/pull/1960) (yoavcloud) +- Add support for VACUUM in Redshift [#2005](https://github.com/apache/datafusion-sqlparser-rs/pull/2005) (yoavcloud) +- Add support for `SEMANTIC_VIEW` table factor [#2009](https://github.com/apache/datafusion-sqlparser-rs/pull/2009) (bombsimon) +- Redshift: Add more copy options [#2008](https://github.com/apache/datafusion-sqlparser-rs/pull/2008) (yoavcloud) +- `GenericDialect`: Support pipe operator [#2012](https://github.com/apache/datafusion-sqlparser-rs/pull/2012) (simonvandel) +- Add SECURE keyword for views in Snowflake [#2004](https://github.com/apache/datafusion-sqlparser-rs/pull/2004) (Vedin) +- Add support for PostgreSQL JSON function 'RETURNING' clauses [#2001](https://github.com/apache/datafusion-sqlparser-rs/pull/2001) (adamchainz) +- Snowflake: Minus char in stage name [#2014](https://github.com/apache/datafusion-sqlparser-rs/pull/2014) (yoavcloud) +- Support wildcard metrics for `SEMANTIC_VIEW` [#2016](https://github.com/apache/datafusion-sqlparser-rs/pull/2016) (bombsimon) +- Allow wilrdacrd for all `SEMANTIC_VIEW` types [#2017](https://github.com/apache/datafusion-sqlparser-rs/pull/2017) (bombsimon) +- Redshift: UNLOAD [#2013](https://github.com/apache/datafusion-sqlparser-rs/pull/2013) (yoavcloud) +- Add support for string literal concatenation [#2003](https://github.com/apache/datafusion-sqlparser-rs/pull/2003) (etgarperets) +- Enable merge queue in sqlparser-rs [#2007](https://github.com/apache/datafusion-sqlparser-rs/pull/2007) (blaginin) +- Merge Queue Test [#2019](https://github.com/apache/datafusion-sqlparser-rs/pull/2019) (blaginin) +- Added derive trait `Copy` to `OrderByOptions` struct [#2021](https://github.com/apache/datafusion-sqlparser-rs/pull/2021) (LucaCappelletti94) +- Moved `CreateTrigger` and `DropTrigger` out of `Statement` enum [#2026](https://github.com/apache/datafusion-sqlparser-rs/pull/2026) (LucaCappelletti94) +- MySQL: Support `CROSS JOIN` constraint [#2025](https://github.com/apache/datafusion-sqlparser-rs/pull/2025) (rs-sac) +- Implemented the `From` method for all clear variants in Statement [#2028](https://github.com/apache/datafusion-sqlparser-rs/pull/2028) (LucaCappelletti94) +- DuckDB: Allow quoted date parts in EXTRACT [#2030](https://github.com/apache/datafusion-sqlparser-rs/pull/2030) (ryanschneider) +- MySQL: Add support for unsigned numeric types [#2031](https://github.com/apache/datafusion-sqlparser-rs/pull/2031) (MohamedAbdeen21) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 17 Yoav Cohen + 6 Chen Chongchen + 6 Michael Victor Zink + 4 etgarperets + 3 Luca Cappelletti + 3 Simon Sawert + 3 Tyler White + 2 Dmitrii Blaginin + 2 Ryan Schneider + 1 Adam Johnson + 1 Artem Osipov + 1 Denys Tsomenko + 1 Marcelo Altmann + 1 Mohamed Abdeen + 1 Ophir LOJKINE + 1 Sidney Cammeresi + 1 Simon Vandel Sillesen + 1 Sven Sauleau + 1 carl + 1 dependabot[bot] + 1 tomershaniii + 1 xitep +``` + +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/derive/Cargo.toml b/derive/Cargo.toml index 7b6477300..549477041 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -18,7 +18,7 @@ [package] name = "sqlparser_derive" description = "Procedural (proc) macros for sqlparser" -version = "0.3.0" +version = "0.4.0" authors = ["sqlparser-rs authors"] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser_derive/" diff --git a/dev/release/generate-changelog.py b/dev/release/generate-changelog.py index 52fd2e548..6f2b7c41c 100755 --- a/dev/release/generate-changelog.py +++ b/dev/release/generate-changelog.py @@ -28,7 +28,8 @@ def print_pulls(repo_name, title, pulls): print() for (pull, commit) in pulls: url = "https://github.com/%7B%7D/pull/%7B%7D".format(repo_name, pull.number) - print("- {} [#{}]({}) ({})".format(pull.title, pull.number, url, commit.author.login)) + author = f"({commit.author.login})" if commit.author else '' + print("- {} [#{}]({}) {}".format(pull.title, pull.number, url, author)) print() @@ -161,4 +162,4 @@ def cli(args=None): generate_changelog(repo, project, args.tag1, args.tag2, args.version) if __name__ == "__main__": - cli() \ No newline at end of file + cli() diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index a567eda9c..280b1bce6 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -1,6 +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 02aa6cc9f..6da6a90d0 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -36,7 +36,7 @@ pub enum EnumMember { Name(String), /// ClickHouse allows to specify an integer value for each enum value. /// - /// [clickhouse](https://clickhouse.com/docs/en/sql-reference/data-types/enum) + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/data-types/enum) NamedValue(String, Expr), } @@ -45,268 +45,367 @@ pub enum EnumMember { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum DataType { - /// Fixed-length character type e.g. CHARACTER(10) + /// Table type in [PostgreSQL], e.g. CREATE FUNCTION RETURNS TABLE(...). + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html + /// [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) + /// Fixed-length char type, e.g. CHAR(10). Char(Option), - /// Character varying type e.g. CHARACTER VARYING(10) + /// Character varying type, e.g. CHARACTER VARYING(10). CharacterVarying(Option), - /// Char varying type e.g. CHAR VARYING(10) + /// Char varying type, e.g. CHAR VARYING(10). CharVarying(Option), - /// Variable-length character type e.g. VARCHAR(10) + /// Variable-length character type, e.g. VARCHAR(10). Varchar(Option), - /// Variable-length character type e.g. NVARCHAR(10) + /// Variable-length character type, e.g. NVARCHAR(10). Nvarchar(Option), - /// Uuid type + /// Uuid type. Uuid, - /// Large character object with optional length e.g. CHARACTER LARGE OBJECT, CHARACTER LARGE OBJECT(1000), [standard] + /// Large character object with optional length, + /// e.g. CHARACTER LARGE OBJECT, CHARACTER LARGE OBJECT(1000), [SQL Standard]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type CharacterLargeObject(Option), - /// Large character object with optional length e.g. CHAR LARGE OBJECT, CHAR LARGE OBJECT(1000), [standard] + /// Large character object with optional length, + /// e.g. CHAR LARGE OBJECT, CHAR LARGE OBJECT(1000), [SQL Standard]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type CharLargeObject(Option), - /// Large character object with optional length e.g. CLOB, CLOB(1000), [standard] + /// Large character object with optional length, + /// e.g. CLOB, CLOB(1000), [SQL Standard]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type /// [Oracle]: https://docs.oracle.com/javadb/10.10.1.2/ref/rrefclob.html Clob(Option), - /// Fixed-length binary type with optional length e.g. [standard], [MS SQL Server] + /// Fixed-length binary type with optional length, + /// see [SQL Standard], [MS SQL Server]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-string-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-string-type /// [MS SQL Server]: https://learn.microsoft.com/pt-br/sql/t-sql/data-types/binary-and-varbinary-transact-sql?view=sql-server-ver16 Binary(Option), - /// Variable-length binary with optional length type e.g. [standard], [MS SQL Server] + /// Variable-length binary with optional length type, + /// see [SQL Standard], [MS SQL Server]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-string-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-string-type /// [MS SQL Server]: https://learn.microsoft.com/pt-br/sql/t-sql/data-types/binary-and-varbinary-transact-sql?view=sql-server-ver16 - Varbinary(Option), - /// Large binary object with optional length e.g. BLOB, BLOB(1000), [standard], [Oracle] + Varbinary(Option), + /// Large binary object with optional length, + /// see [SQL Standard], [Oracle]. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-large-object-string-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-large-object-string-type /// [Oracle]: https://docs.oracle.com/javadb/10.8.3.0/ref/rrefblob.html Blob(Option), - /// [MySQL] blob with up to 2**8 bytes + /// [MySQL] blob with up to 2**8 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html TinyBlob, - /// [MySQL] blob with up to 2**24 bytes + /// [MySQL] blob with up to 2**24 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html MediumBlob, - /// [MySQL] blob with up to 2**32 bytes + /// [MySQL] blob with up to 2**32 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html LongBlob, /// Variable-length binary data with optional length. /// - /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#bytes_type + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#bytes_type Bytes(Option), - /// Numeric type with optional precision and scale e.g. NUMERIC(10,2), [standard][1] + /// Numeric type with optional precision and scale, e.g. NUMERIC(10,2), [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type Numeric(ExactNumberInfo), - /// Decimal type with optional precision and scale e.g. DECIMAL(10,2), [standard][1] + /// Decimal type with optional precision and scale, e.g. DECIMAL(10,2), [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type Decimal(ExactNumberInfo), - /// [BigNumeric] type used in BigQuery + /// [MySQL] unsigned decimal with optional precision and scale, e.g. DECIMAL UNSIGNED or DECIMAL(10,2) UNSIGNED. + /// Note: Using UNSIGNED with DECIMAL is deprecated in recent versions of MySQL. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/numeric-type-syntax.html + DecimalUnsigned(ExactNumberInfo), + /// [BigNumeric] type used in BigQuery. /// /// [BigNumeric]: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#bignumeric_literals BigNumeric(ExactNumberInfo), - /// This is alias for `BigNumeric` type used in BigQuery + /// This is alias for `BigNumeric` type used in BigQuery. /// /// [BigDecimal]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#decimal_types BigDecimal(ExactNumberInfo), - /// Dec type with optional precision and scale e.g. DEC(10,2), [standard][1] + /// Dec type with optional precision and scale, e.g. DEC(10,2), [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type Dec(ExactNumberInfo), - /// Floating point with optional precision e.g. FLOAT(8) - Float(Option), - /// Tiny integer with optional display width e.g. TINYINT or TINYINT(3) + /// [MySQL] unsigned decimal (DEC alias) with optional precision and scale, e.g. DEC UNSIGNED or DEC(10,2) UNSIGNED. + /// Note: Using UNSIGNED with DEC is deprecated in recent versions of MySQL. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/numeric-type-syntax.html + DecUnsigned(ExactNumberInfo), + /// Floating point with optional precision and scale, e.g. FLOAT, FLOAT(8), or FLOAT(8,2). + Float(ExactNumberInfo), + /// [MySQL] unsigned floating point with optional precision and scale, e.g. + /// FLOAT UNSIGNED, FLOAT(10) UNSIGNED or FLOAT(10,2) UNSIGNED. + /// Note: Using UNSIGNED with FLOAT is deprecated in recent versions of MySQL. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/numeric-type-syntax.html + FloatUnsigned(ExactNumberInfo), + /// Tiny integer with optional display width, e.g. TINYINT or TINYINT(3). TinyInt(Option), - /// Unsigned tiny integer with optional display width e.g. TINYINT UNSIGNED or TINYINT(3) UNSIGNED - UnsignedTinyInt(Option), - /// Int2 as alias for SmallInt in [postgresql] - /// Note: Int2 mean 2 bytes in postgres (not 2 bits) - /// Int2 with optional display width e.g. INT2 or INT2(5) + /// Unsigned tiny integer with optional display width, + /// e.g. TINYINT UNSIGNED or TINYINT(3) UNSIGNED. + TinyIntUnsigned(Option), + /// Unsigned tiny integer, e.g. UTINYINT + UTinyInt, + /// Int2 is an alias for SmallInt in [PostgreSQL]. + /// Note: Int2 means 2 bytes in PostgreSQL (not 2 bits). + /// Int2 with optional display width, e.g. INT2 or INT2(5). /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Int2(Option), - /// Unsigned Int2 with optional display width e.g. INT2 Unsigned or INT2(5) Unsigned - UnsignedInt2(Option), - /// Small integer with optional display width e.g. SMALLINT or SMALLINT(5) + /// Unsigned Int2 with optional display width, e.g. INT2 UNSIGNED or INT2(5) UNSIGNED. + Int2Unsigned(Option), + /// Small integer with optional display width, e.g. SMALLINT or SMALLINT(5). SmallInt(Option), - /// Unsigned small integer with optional display width e.g. SMALLINT UNSIGNED or SMALLINT(5) UNSIGNED - UnsignedSmallInt(Option), - /// MySQL medium integer ([1]) with optional display width e.g. MEDIUMINT or MEDIUMINT(5) + /// Unsigned small integer with optional display width, + /// e.g. SMALLINT UNSIGNED or SMALLINT(5) UNSIGNED. + SmallIntUnsigned(Option), + /// Unsigned small integer, e.g. USMALLINT. + USmallInt, + /// MySQL medium integer ([1]) with optional display width, + /// e.g. MEDIUMINT or MEDIUMINT(5). /// /// [1]: https://dev.mysql.com/doc/refman/8.0/en/integer-types.html MediumInt(Option), - /// Unsigned medium integer ([1]) with optional display width e.g. MEDIUMINT UNSIGNED or MEDIUMINT(5) UNSIGNED + /// Unsigned medium integer ([1]) with optional display width, + /// e.g. MEDIUMINT UNSIGNED or MEDIUMINT(5) UNSIGNED. /// /// [1]: https://dev.mysql.com/doc/refman/8.0/en/integer-types.html - UnsignedMediumInt(Option), - /// Int with optional display width e.g. INT or INT(11) + MediumIntUnsigned(Option), + /// Int with optional display width, e.g. INT or INT(11). Int(Option), - /// Int4 as alias for Integer in [postgresql] - /// Note: Int4 mean 4 bytes in postgres (not 4 bits) - /// Int4 with optional display width e.g. Int4 or Int4(11) + /// Int4 is an alias for Integer in [PostgreSQL]. + /// Note: Int4 means 4 bytes in PostgreSQL (not 4 bits). + /// Int4 with optional display width, e.g. Int4 or Int4(11). /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Int4(Option), - /// Int8 as alias for Bigint in [postgresql] and integer type in [clickhouse] - /// Note: Int8 mean 8 bytes in [postgresql] (not 8 bits) - /// Int8 with optional display width e.g. INT8 or INT8(11) - /// Note: Int8 mean 8 bits in [clickhouse] + /// Int8 is an alias for BigInt in [PostgreSQL] and Integer type in [ClickHouse]. + /// Int8 with optional display width, e.g. INT8 or INT8(11). + /// Note: Int8 means 8 bytes in [PostgreSQL], but 8 bits in [ClickHouse]. /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int8(Option), - /// Integer type in [clickhouse] - /// Note: Int16 mean 16 bits in [clickhouse] + /// Integer type in [ClickHouse]. + /// Note: Int16 means 16 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int16, - /// Integer type in [clickhouse] - /// Note: Int16 mean 32 bits in [clickhouse] + /// Integer type in [ClickHouse]. + /// Note: Int32 means 32 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int32, - /// Integer type in [bigquery], [clickhouse] + /// Integer type in [BigQuery], [ClickHouse]. /// - /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#integer_types - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#integer_types + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int64, - /// Integer type in [clickhouse] - /// Note: Int128 mean 128 bits in [clickhouse] + /// Integer type in [ClickHouse]. + /// Note: Int128 means 128 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int128, - /// Integer type in [clickhouse] - /// Note: Int256 mean 256 bits in [clickhouse] + /// Integer type in [ClickHouse]. + /// Note: Int256 means 256 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint Int256, - /// Integer with optional display width e.g. INTEGER or INTEGER(11) + /// Integer with optional display width, e.g. INTEGER or INTEGER(11). Integer(Option), - /// Unsigned int with optional display width e.g. INT UNSIGNED or INT(11) UNSIGNED - UnsignedInt(Option), - /// Unsigned int4 with optional display width e.g. INT4 UNSIGNED or INT4(11) UNSIGNED - UnsignedInt4(Option), - /// Unsigned integer with optional display width e.g. INTEGER UNSIGNED or INTEGER(11) UNSIGNED - UnsignedInteger(Option), - /// Unsigned integer type in [clickhouse] - /// Note: UInt8 mean 8 bits in [clickhouse] - /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// Unsigned int with optional display width, e.g. INT UNSIGNED or INT(11) UNSIGNED. + IntUnsigned(Option), + /// Unsigned int4 with optional display width, e.g. INT4 UNSIGNED or INT4(11) UNSIGNED. + Int4Unsigned(Option), + /// Unsigned integer with optional display width, e.g. INTEGER UNSIGNED or INTEGER(11) UNSIGNED. + IntegerUnsigned(Option), + /// 128-bit integer type, e.g. HUGEINT. + HugeInt, + /// Unsigned 128-bit integer type, e.g. UHUGEINT. + UHugeInt, + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt8 means 8 bits in [ClickHouse]. + /// + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt8, - /// Unsigned integer type in [clickhouse] - /// Note: UInt16 mean 16 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt16 means 16 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt16, - /// Unsigned integer type in [clickhouse] - /// Note: UInt32 mean 32 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt32 means 32 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt32, - /// Unsigned integer type in [clickhouse] - /// Note: UInt64 mean 64 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt64 means 64 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt64, - /// Unsigned integer type in [clickhouse] - /// Note: UInt128 mean 128 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt128 means 128 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt128, - /// Unsigned integer type in [clickhouse] - /// Note: UInt256 mean 256 bits in [clickhouse] + /// Unsigned integer type in [ClickHouse]. + /// Note: UInt256 means 256 bits in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint UInt256, - /// Big integer with optional display width e.g. BIGINT or BIGINT(20) + /// Big integer with optional display width, e.g. BIGINT or BIGINT(20). BigInt(Option), - /// Unsigned big integer with optional display width e.g. BIGINT UNSIGNED or BIGINT(20) UNSIGNED - UnsignedBigInt(Option), - /// Unsigned Int8 with optional display width e.g. INT8 UNSIGNED or INT8(11) UNSIGNED - UnsignedInt8(Option), - /// Float4 as alias for Real in [postgresql] + /// Unsigned big integer with optional display width, e.g. BIGINT UNSIGNED or BIGINT(20) UNSIGNED. + BigIntUnsigned(Option), + /// Unsigned big integer, e.g. UBIGINT. + UBigInt, + /// Unsigned Int8 with optional display width, e.g. INT8 UNSIGNED or INT8(11) UNSIGNED. + Int8Unsigned(Option), + /// Signed integer as used in [MySQL CAST] target types, without optional `INTEGER` suffix, + /// e.g. `SIGNED` + /// + /// [MySQL CAST]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html + Signed, + /// Signed integer as used in [MySQL CAST] target types, with optional `INTEGER` suffix, + /// e.g. `SIGNED INTEGER` + /// + /// [MySQL CAST]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html + SignedInteger, + /// Signed integer as used in [MySQL CAST] target types, without optional `INTEGER` suffix, + /// e.g. `SIGNED` /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [MySQL CAST]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html + Unsigned, + /// Unsigned integer as used in [MySQL CAST] target types, with optional `INTEGER` suffix, + /// e.g. `UNSIGNED INTEGER`. + /// + /// [MySQL CAST]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html + UnsignedInteger, + /// Float4 is an alias for Real in [PostgreSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Float4, - /// Floating point in [clickhouse] + /// Floating point in [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/float + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/float Float32, - /// Floating point in [bigquery] + /// Floating point in [BigQuery]. /// - /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#floating_point_types - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/float + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#floating_point_types + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/float Float64, - /// Floating point e.g. REAL + /// Floating point, e.g. REAL. Real, - /// Float8 as alias for Double in [postgresql] + /// [MySQL] unsigned real, e.g. REAL UNSIGNED. + /// Note: Using UNSIGNED with REAL is deprecated in recent versions of MySQL. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/numeric-type-syntax.html + RealUnsigned, + /// Float8 is an alias for Double in [PostgreSQL]. /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Float8, /// Double Double(ExactNumberInfo), - /// Double PRECISION e.g. [standard], [postgresql] + /// [MySQL] unsigned double precision with optional precision, e.g. DOUBLE UNSIGNED or DOUBLE(10,2) UNSIGNED. + /// Note: Using UNSIGNED with DOUBLE is deprecated in recent versions of MySQL. /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#approximate-numeric-type - /// [postgresql]: https://www.postgresql.org/docs/current/datatype-numeric.html + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/numeric-type-syntax.html + DoubleUnsigned(ExactNumberInfo), + /// Double Precision, see [SQL Standard], [PostgreSQL]. + /// + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#approximate-numeric-type + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype-numeric.html DoublePrecision, - /// Bool as alias for Boolean in [postgresql] + /// [MySQL] unsigned double precision, e.g. DOUBLE PRECISION UNSIGNED. + /// Note: Using UNSIGNED with DOUBLE PRECISION is deprecated in recent versions of MySQL. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/numeric-type-syntax.html + DoublePrecisionUnsigned, + /// Bool is an alias for Boolean, see [PostgreSQL]. /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Bool, - /// Boolean + /// Boolean type. Boolean, - /// Date + /// Date type. Date, - /// Date32 with the same range as Datetime64 + /// Date32 with the same range as Datetime64. /// /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/date32 Date32, - /// Time with optional time precision and time zone information e.g. [standard][1]. + /// Time with optional time precision and time zone information, see [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type Time(Option, TimezoneInfo), - /// Datetime with optional time precision e.g. [MySQL][1]. + /// Datetime with optional time precision, see [MySQL][1]. /// /// [1]: https://dev.mysql.com/doc/refman/8.0/en/datetime.html Datetime(Option), - /// Datetime with time precision and optional timezone e.g. [ClickHouse][1]. + /// Datetime with time precision and optional timezone, see [ClickHouse][1]. /// /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/datetime64 Datetime64(u64, Option), - /// Timestamp with optional time precision and time zone information e.g. [standard][1]. + /// Timestamp with optional time precision and time zone information, see [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type Timestamp(Option, TimezoneInfo), - /// Interval - Interval, - /// JSON type + /// Databricks timestamp without time zone. See [1]. + /// + /// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type + TimestampNtz(Option), + /// Interval type. + 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 + /// Binary JSON type. JSONB, - /// Regclass used in postgresql serial + /// Regclass used in [PostgreSQL] serial. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html Regclass, - /// Text + /// Text type. Text, - /// [MySQL] text with up to 2**8 bytes + /// [MySQL] text with up to 2**8 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html TinyText, - /// [MySQL] text with up to 2**24 bytes + /// [MySQL] text with up to 2**24 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html MediumText, - /// [MySQL] text with up to 2**32 bytes + /// [MySQL] text with up to 2**32 bytes. /// /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html LongText, @@ -316,67 +415,85 @@ pub enum DataType { /// /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/fixedstring FixedString(u64), - /// Bytea + /// Bytea type, see [PostgreSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype-bit.html Bytea, - /// Bit string, e.g. [Postgres], [MySQL], or [MSSQL] + /// Bit string, see [PostgreSQL], [MySQL], or [MSSQL]. /// - /// [Postgres]: https://www.postgresql.org/docs/current/datatype-bit.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype-bit.html /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/bit-type.html /// [MSSQL]: https://learn.microsoft.com/en-us/sql/t-sql/data-types/bit-transact-sql?view=sql-server-ver16 Bit(Option), - /// Variable-length bit string e.g. [Postgres] + /// `BIT VARYING(n)`: Variable-length bit string, see [PostgreSQL]. /// - /// [Postgres]: https://www.postgresql.org/docs/current/datatype-bit.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype-bit.html BitVarying(Option), - /// Custom type such as enums + /// `VARBIT(n)`: Variable-length bit string. [PostgreSQL] alias for `BIT VARYING`. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html + VarBit(Option), + /// Custom types. Custom(ObjectName, Vec), - /// Arrays + /// Arrays. Array(ArrayElemTypeDef), - /// Map + /// Map, see [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/map + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/map Map(Box, Box), - /// Tuple + /// Tuple, see [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple Tuple(Vec), - /// Nested + /// Nested type, see [ClickHouse]. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nested-data-structures/nested + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nested-data-structures/nested Nested(Vec), - /// Enums + /// Enum type. Enum(Vec, Option), - /// Set + /// Set type. Set(Vec), - /// Struct + /// Struct type, see [Hive], [BigQuery]. /// - /// [hive]: https://docs.cloudera.com/cdw-runtime/cloud/impala-sql-reference/topics/impala-struct.html - /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type + /// [Hive]: https://docs.cloudera.com/cdw-runtime/cloud/impala-sql-reference/topics/impala-struct.html + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type Struct(Vec, StructBracketKind), - /// Union + /// Union type, see [DuckDB]. /// - /// [duckdb]: https://duckdb.org/docs/sql/data_types/union.html + /// [DuckDB]: https://duckdb.org/docs/sql/data_types/union.html Union(Vec), /// Nullable - special marker NULL represents in ClickHouse as a data type. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nullable + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nullable Nullable(Box), /// LowCardinality - changes the internal representation of other data types to be dictionary-encoded. /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/lowcardinality + /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/lowcardinality LowCardinality(Box), /// No type specified - only used with /// [`SQLiteDialect`](crate::dialect::SQLiteDialect), from statements such /// as `CREATE TABLE t1 (a)`. Unspecified, - /// Trigger data type, returned by functions associated with triggers + /// Trigger data type, returned by functions associated with triggers, see [PostgreSQL]. /// - /// [postgresql]: https://www.postgresql.org/docs/current/plpgsql-trigger.html + /// [PostgreSQL]: https://www.postgresql.org/docs/current/plpgsql-trigger.html Trigger, - /// Any data type, used in BigQuery UDF definitions for templated parameters + /// Any data type, used in BigQuery UDF definitions for templated parameters, see [BigQuery]. /// - /// [bigquery]: https://cloud.google.com/bigquery/docs/user-defined-functions#templated-sql-udf-parameters + /// [BigQuery]: https://cloud.google.com/bigquery/docs/user-defined-functions#templated-sql-udf-parameters AnyType, + /// Geometric type, see [PostgreSQL]. + /// + /// [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 { @@ -399,9 +516,7 @@ impl fmt::Display for DataType { } DataType::Clob(size) => format_type_with_optional_length(f, "CLOB", size, false), DataType::Binary(size) => format_type_with_optional_length(f, "BINARY", size, false), - DataType::Varbinary(size) => { - format_type_with_optional_length(f, "VARBINARY", size, false) - } + DataType::Varbinary(size) => format_varbinary_type(f, "VARBINARY", size), DataType::Blob(size) => format_type_with_optional_length(f, "BLOB", size, false), DataType::TinyBlob => write!(f, "TINYBLOB"), DataType::MediumBlob => write!(f, "MEDIUMBLOB"), @@ -413,38 +528,45 @@ impl fmt::Display for DataType { DataType::Decimal(info) => { write!(f, "DECIMAL{info}") } + DataType::DecimalUnsigned(info) => { + write!(f, "DECIMAL{info} UNSIGNED") + } DataType::Dec(info) => { write!(f, "DEC{info}") } + DataType::DecUnsigned(info) => { + write!(f, "DEC{info} UNSIGNED") + } DataType::BigNumeric(info) => write!(f, "BIGNUMERIC{info}"), DataType::BigDecimal(info) => write!(f, "BIGDECIMAL{info}"), - DataType::Float(size) => format_type_with_optional_length(f, "FLOAT", size, false), + DataType::Float(info) => write!(f, "FLOAT{info}"), + DataType::FloatUnsigned(info) => write!(f, "FLOAT{info} UNSIGNED"), DataType::TinyInt(zerofill) => { format_type_with_optional_length(f, "TINYINT", zerofill, false) } - DataType::UnsignedTinyInt(zerofill) => { + DataType::TinyIntUnsigned(zerofill) => { format_type_with_optional_length(f, "TINYINT", zerofill, true) } DataType::Int2(zerofill) => { format_type_with_optional_length(f, "INT2", zerofill, false) } - DataType::UnsignedInt2(zerofill) => { + DataType::Int2Unsigned(zerofill) => { format_type_with_optional_length(f, "INT2", zerofill, true) } DataType::SmallInt(zerofill) => { format_type_with_optional_length(f, "SMALLINT", zerofill, false) } - DataType::UnsignedSmallInt(zerofill) => { + DataType::SmallIntUnsigned(zerofill) => { format_type_with_optional_length(f, "SMALLINT", zerofill, true) } DataType::MediumInt(zerofill) => { format_type_with_optional_length(f, "MEDIUMINT", zerofill, false) } - DataType::UnsignedMediumInt(zerofill) => { + DataType::MediumIntUnsigned(zerofill) => { format_type_with_optional_length(f, "MEDIUMINT", zerofill, true) } DataType::Int(zerofill) => format_type_with_optional_length(f, "INT", zerofill, false), - DataType::UnsignedInt(zerofill) => { + DataType::IntUnsigned(zerofill) => { format_type_with_optional_length(f, "INT", zerofill, true) } DataType::Int4(zerofill) => { @@ -468,24 +590,39 @@ impl fmt::Display for DataType { DataType::Int256 => { write!(f, "Int256") } - DataType::UnsignedInt4(zerofill) => { + DataType::HugeInt => { + write!(f, "HUGEINT") + } + DataType::Int4Unsigned(zerofill) => { format_type_with_optional_length(f, "INT4", zerofill, true) } DataType::Integer(zerofill) => { format_type_with_optional_length(f, "INTEGER", zerofill, false) } - DataType::UnsignedInteger(zerofill) => { + DataType::IntegerUnsigned(zerofill) => { format_type_with_optional_length(f, "INTEGER", zerofill, true) } DataType::BigInt(zerofill) => { format_type_with_optional_length(f, "BIGINT", zerofill, false) } - DataType::UnsignedBigInt(zerofill) => { + DataType::BigIntUnsigned(zerofill) => { format_type_with_optional_length(f, "BIGINT", zerofill, true) } - DataType::UnsignedInt8(zerofill) => { + DataType::Int8Unsigned(zerofill) => { format_type_with_optional_length(f, "INT8", zerofill, true) } + DataType::UTinyInt => { + write!(f, "UTINYINT") + } + DataType::USmallInt => { + write!(f, "USMALLINT") + } + DataType::UBigInt => { + write!(f, "UBIGINT") + } + DataType::UHugeInt => { + write!(f, "UHUGEINT") + } DataType::UInt8 => { write!(f, "UInt8") } @@ -504,13 +641,28 @@ impl fmt::Display for DataType { DataType::UInt256 => { write!(f, "UInt256") } + DataType::Signed => { + write!(f, "SIGNED") + } + DataType::SignedInteger => { + write!(f, "SIGNED INTEGER") + } + DataType::Unsigned => { + write!(f, "UNSIGNED") + } + DataType::UnsignedInteger => { + write!(f, "UNSIGNED INTEGER") + } DataType::Real => write!(f, "REAL"), + DataType::RealUnsigned => write!(f, "REAL UNSIGNED"), DataType::Float4 => write!(f, "FLOAT4"), DataType::Float32 => write!(f, "Float32"), DataType::Float64 => write!(f, "FLOAT64"), DataType::Double(info) => write!(f, "DOUBLE{info}"), + DataType::DoubleUnsigned(info) => write!(f, "DOUBLE{info} UNSIGNED"), DataType::Float8 => write!(f, "FLOAT8"), DataType::DoublePrecision => write!(f, "DOUBLE PRECISION"), + DataType::DoublePrecisionUnsigned => write!(f, "DOUBLE PRECISION UNSIGNED"), DataType::Bool => write!(f, "BOOL"), DataType::Boolean => write!(f, "BOOLEAN"), DataType::Date => write!(f, "DATE"), @@ -524,6 +676,9 @@ impl fmt::Display for DataType { DataType::Timestamp(precision, timezone_info) => { format_datetime_precision_and_tz(f, "TIMESTAMP", precision, timezone_info) } + DataType::TimestampNtz(precision) => { + format_type_with_optional_length(f, "TIMESTAMP_NTZ", precision, false) + } DataType::Datetime64(precision, timezone) => { format_clickhouse_datetime_precision_and_timezone( f, @@ -532,7 +687,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"), @@ -546,6 +710,7 @@ impl fmt::Display for DataType { DataType::BitVarying(size) => { format_type_with_optional_length(f, "BIT VARYING", size, false) } + DataType::VarBit(size) => format_type_with_optional_length(f, "VARBIT", size, false), DataType::Array(ty) => match ty { ArrayElemTypeDef::None => write!(f, "ARRAY"), ArrayElemTypeDef::SquareBracket(t, None) => write!(f, "{t}[]"), @@ -562,7 +727,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, "(")?; @@ -610,16 +775,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)) @@ -630,6 +795,20 @@ impl fmt::Display for DataType { DataType::Unspecified => Ok(()), DataType::Trigger => write!(f, "TRIGGER"), DataType::AnyType => write!(f, "ANY TYPE"), + 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"), } } } @@ -662,6 +841,18 @@ fn format_character_string_type( Ok(()) } +fn format_varbinary_type( + f: &mut fmt::Formatter, + sql_type: &str, + size: &Option, +) -> fmt::Result { + write!(f, "{sql_type}")?; + if let Some(size) = size { + write!(f, "({size})")?; + } + Ok(()) +} + fn format_datetime_precision_and_tz( f: &mut fmt::Formatter, sql_type: &'static str, @@ -719,19 +910,19 @@ pub enum StructBracketKind { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TimezoneInfo { - /// No information about time zone. E.g., TIMESTAMP + /// No information about time zone, e.g. TIMESTAMP None, - /// Temporal type 'WITH TIME ZONE'. E.g., TIMESTAMP WITH TIME ZONE, [standard], [Oracle] + /// Temporal type 'WITH TIME ZONE', e.g. TIMESTAMP WITH TIME ZONE, [SQL Standard], [Oracle] /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type /// [Oracle]: https://docs.oracle.com/en/database/oracle/oracle-database/12.2/nlspg/datetime-data-types-and-time-zone-support.html#GUID-3F1C388E-C651-43D5-ADBC-1A49E5C2CA05 WithTimeZone, - /// Temporal type 'WITHOUT TIME ZONE'. E.g., TIME WITHOUT TIME ZONE, [standard], [Postgresql] + /// Temporal type 'WITHOUT TIME ZONE', e.g. TIME WITHOUT TIME ZONE, [SQL Standard], [Postgresql] /// - /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type + /// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type /// [Postgresql]: https://www.postgresql.org/docs/current/datatype-datetime.html WithoutTimeZone, - /// Postgresql specific `WITH TIME ZONE` formatting, for both TIME and TIMESTAMP. E.g., TIMETZ, [Postgresql] + /// Postgresql specific `WITH TIME ZONE` formatting, for both TIME and TIMESTAMP, e.g. TIMETZ, [Postgresql] /// /// [Postgresql]: https://www.postgresql.org/docs/current/datatype-datetime.html Tz, @@ -759,20 +950,62 @@ 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 [standard]. +/// following the 2016 [SQL Standard]. /// -/// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type +/// [SQL Standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type #[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 ExactNumberInfo { - /// No additional information e.g. `DECIMAL` + /// No additional information, e.g. `DECIMAL` None, - /// Only precision information e.g. `DECIMAL(10)` + /// Only precision information, e.g. `DECIMAL(10)` Precision(u64), - /// Precision and scale information e.g. `DECIMAL(10,2)` - PrecisionAndScale(u64, u64), + /// Precision and scale information, e.g. `DECIMAL(10,2)` + PrecisionAndScale(u64, i64), } impl fmt::Display for ExactNumberInfo { @@ -812,7 +1045,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}")?; } @@ -825,7 +1058,7 @@ impl fmt::Display for CharacterLength { } } -/// Possible units for characters, initially based on 2016 ANSI [standard][1]. +/// Possible units for characters, initially based on 2016 ANSI [SQL Standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#char-length-units #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -851,6 +1084,32 @@ impl fmt::Display for CharLengthUnits { } } +#[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 BinaryLength { + IntegerLength { + /// Default (if VARYING) + length: u64, + }, + /// VARBINARY(MAX) used in T-SQL (Microsoft SQL Server) + Max, +} + +impl fmt::Display for BinaryLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BinaryLength::IntegerLength { length } => { + write!(f, "{length}")?; + } + BinaryLength::Max => { + write!(f, "MAX")?; + } + } + Ok(()) + } +} + /// Represents the data type of the elements in an array (if any) as well as /// the syntax used to declare the array. /// @@ -868,3 +1127,34 @@ pub enum ArrayElemTypeDef { /// `Array(Int64)` Parenthesis(Box), } + +/// Represents different types of geometric shapes which are commonly used in +/// PostgreSQL/Redshift for spatial operations and geometry-related computations. +/// +/// [PostgreSQL]: https://www.postgresql.org/docs/9.5/functions-geometry.html +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum GeometricTypeKind { + Point, + Line, + LineSegment, + GeometricBox, + GeometricPath, + Polygon, + Circle, +} + +impl fmt::Display for GeometricTypeKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GeometricTypeKind::Point => write!(f, "point"), + GeometricTypeKind::Line => write!(f, "line"), + GeometricTypeKind::LineSegment => write!(f, "lseg"), + GeometricTypeKind::GeometricBox => write!(f, "box"), + GeometricTypeKind::GeometricPath => write!(f, "path"), + GeometricTypeKind::Polygon => write!(f, "polygon"), + GeometricTypeKind::Circle => write!(f, "circle"), + } + } +} diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 735ab0cce..d04875a73 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -28,8 +28,9 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use super::{display_comma_separated, Expr, Ident, Password}; +use super::{display_comma_separated, Expr, Ident, Password, Spanned}; use crate::ast::{display_separated, ObjectName}; +use crate::tokenizer::Span; /// An option in `ROLE` statement. /// @@ -173,7 +174,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 +188,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 +219,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"), } } @@ -252,3 +253,113 @@ impl fmt::Display for SecondaryRoles { } } } + +/// CREATE ROLE statement +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrole.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 CreateRole { + pub names: Vec, + pub if_not_exists: bool, + // Postgres + pub login: Option, + pub inherit: Option, + pub bypassrls: Option, + pub password: Option, + pub superuser: Option, + pub create_db: Option, + pub create_role: Option, + pub replication: Option, + pub connection_limit: Option, + pub valid_until: Option, + pub in_role: Vec, + pub in_group: Vec, + pub role: Vec, + pub user: Vec, + pub admin: Vec, + // MSSQL + pub authorization_owner: Option, +} + +impl fmt::Display for CreateRole { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", + if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + names = display_separated(&self.names, ", "), + superuser = match self.superuser { + Some(true) => " SUPERUSER", + Some(false) => " NOSUPERUSER", + None => "" + }, + create_db = match self.create_db { + Some(true) => " CREATEDB", + Some(false) => " NOCREATEDB", + None => "" + }, + create_role = match self.create_role { + Some(true) => " CREATEROLE", + Some(false) => " NOCREATEROLE", + None => "" + }, + inherit = match self.inherit { + Some(true) => " INHERIT", + Some(false) => " NOINHERIT", + None => "" + }, + login = match self.login { + Some(true) => " LOGIN", + Some(false) => " NOLOGIN", + None => "" + }, + replication = match self.replication { + Some(true) => " REPLICATION", + Some(false) => " NOREPLICATION", + None => "" + }, + bypassrls = match self.bypassrls { + Some(true) => " BYPASSRLS", + Some(false) => " NOBYPASSRLS", + None => "" + } + )?; + if let Some(limit) = &self.connection_limit { + write!(f, " CONNECTION LIMIT {limit}")?; + } + match &self.password { + Some(Password::Password(pass)) => write!(f, " PASSWORD {pass}")?, + Some(Password::NullPassword) => write!(f, " PASSWORD NULL")?, + None => {} + }; + if let Some(until) = &self.valid_until { + write!(f, " VALID UNTIL {until}")?; + } + if !self.in_role.is_empty() { + write!(f, " IN ROLE {}", display_comma_separated(&self.in_role))?; + } + if !self.in_group.is_empty() { + write!(f, " IN GROUP {}", display_comma_separated(&self.in_group))?; + } + if !self.role.is_empty() { + write!(f, " ROLE {}", display_comma_separated(&self.role))?; + } + if !self.user.is_empty() { + write!(f, " USER {}", display_comma_separated(&self.user))?; + } + if !self.admin.is_empty() { + write!(f, " ADMIN {}", display_comma_separated(&self.admin))?; + } + if let Some(owner) = &self.authorization_owner { + write!(f, " AUTHORIZATION {owner}")?; + } + Ok(()) + } +} + +impl Spanned for CreateRole { + fn span(&self) -> Span { + Span::empty() + } +} diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1b5ccda26..1ae24a7f4 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -19,8 +19,8 @@ //! (commonly referred to as Data Definition Language, or DDL) #[cfg(not(feature = "std"))] -use alloc::{boxed::Box, string::String, vec::Vec}; -use core::fmt::{self, Write}; +use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; +use core::fmt::{self, Display, Write}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -30,21 +30,93 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, CreateFunctionBody, CreateFunctionUsing, DataType, - Expr, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, - Ident, MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, - SequenceOptions, SqlOption, Tag, Value, + display_comma_separated, display_separated, + table_constraints::{ + CheckConstraint, ForeignKeyConstraint, PrimaryKeyConstraint, TableConstraint, + UniqueConstraint, + }, + ArgMode, AttachedToken, CommentDef, ConditionalStatements, CreateFunctionBody, + CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, CreateViewParams, DataType, Expr, + FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDesc, FunctionDeterminismSpecifier, + FunctionParallel, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, + HiveSetLocation, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, + OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, + RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, + Tag, TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, 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 From for IndexColumn { + fn from(c: Ident) -> Self { + Self { + column: OrderByExpr::from(c), + operator_class: None, + } + } +} + +impl<'a> From<&'a str> for IndexColumn { + fn from(c: &'a str) -> Self { + let ident = Ident::new(c); + ident.into() + } +} + +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]`. @@ -65,7 +137,6 @@ pub enum AlterTableOperation { name: Ident, select: ProjectionSelect, }, - /// `DROP PROJECTION [IF EXISTS] name` /// /// Note: this is a ClickHouse-specific operation. @@ -74,7 +145,6 @@ pub enum AlterTableOperation { if_exists: bool, name: Ident, }, - /// `MATERIALIZE PROJECTION [IF EXISTS] name [IN PARTITION partition_name]` /// /// Note: this is a ClickHouse-specific operation. @@ -84,7 +154,6 @@ pub enum AlterTableOperation { name: Ident, partition: Option, }, - /// `CLEAR PROJECTION [IF EXISTS] name [IN PARTITION partition_name]` /// /// Note: this is a ClickHouse-specific operation. @@ -94,7 +163,6 @@ pub enum AlterTableOperation { name: Ident, partition: Option, }, - /// `DISABLE ROW LEVEL SECURITY` /// /// Note: this is a PostgreSQL-specific operation. @@ -117,15 +185,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. @@ -154,8 +223,25 @@ pub enum AlterTableOperation { }, /// `DROP PRIMARY KEY` /// - /// Note: this is a MySQL-specific operation. - 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 ` + /// + /// [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 + DropIndex { + name: Ident, + }, /// `ENABLE ALWAYS RULE rewrite_rule_name` /// /// Note: this is a PostgreSQL-specific operation. @@ -201,6 +287,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, @@ -217,7 +310,7 @@ pub enum AlterTableOperation { }, /// `RENAME TO ` RenameTable { - table_name: ObjectName, + table_name: RenameTableNameKind, }, // CHANGE [ COLUMN ] [ ] ChangeColumn { @@ -272,6 +365,60 @@ pub enum AlterTableOperation { DropClusteringKey, SuspendRecluster, ResumeRecluster, + /// `REFRESH` + /// + /// Note: this is Snowflake specific for dynamic tables + Refresh, + /// `SUSPEND` + /// + /// Note: this is Snowflake specific for dynamic tables + Suspend, + /// `RESUME` + /// + /// Note: this is Snowflake specific for dynamic tables + Resume, + /// `ALGORITHM [=] { DEFAULT | INSTANT | INPLACE | COPY }` + /// + /// [MySQL]-specific table alter algorithm. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html + Algorithm { + equals: bool, + algorithm: AlterTableAlgorithm, + }, + + /// `LOCK [=] { DEFAULT | NONE | SHARED | EXCLUSIVE }` + /// + /// [MySQL]-specific table alter lock. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html + Lock { + equals: bool, + lock: AlterTableLock, + }, + /// `AUTO_INCREMENT [=] ` + /// + /// [MySQL]-specific table option for raising current auto increment value. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html + AutoIncrement { + 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 @@ -317,6 +464,54 @@ impl fmt::Display for AlterPolicyOperation { } } +/// [MySQL] `ALTER TABLE` algorithm. +/// +/// [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 AlterTableAlgorithm { + Default, + Instant, + Inplace, + Copy, +} + +impl fmt::Display for AlterTableAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + Self::Default => "DEFAULT", + Self::Instant => "INSTANT", + Self::Inplace => "INPLACE", + Self::Copy => "COPY", + }) + } +} + +/// [MySQL] `ALTER TABLE` lock. +/// +/// [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 AlterTableLock { + Default, + None, + Shared, + Exclusive, +} + +impl fmt::Display for AlterTableLock { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + Self::Default => "DEFAULT", + Self::None => "NONE", + Self::Shared => "SHARED", + Self::Exclusive => "EXCLUSIVE", + }) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -330,7 +525,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"), @@ -338,6 +533,23 @@ impl fmt::Display for Owner { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterConnectorOwner { + User(Ident), + Role(Ident), +} + +impl fmt::Display for AlterConnectorOwner { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterConnectorOwner::User(ident) => write!(f, "USER {ident}"), + AlterConnectorOwner::Role(ident) => write!(f, "ROLE {ident}"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -357,7 +569,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, @@ -388,14 +609,22 @@ 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!( + f, + "ALGORITHM {}{}", + if *equals { "= " } else { "" }, + algorithm + ) } AlterTableOperation::DropProjection { if_exists, name } => { write!(f, "DROP PROJECTION")?; if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name) + write!(f, " {name}") } AlterTableOperation::MaterializeProjection { if_exists, @@ -406,9 +635,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(()) } @@ -421,9 +650,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(()) } @@ -455,32 +684,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 { 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::DropPrimaryKey => write!(f, "DROP PRIMARY KEY"), + 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}") } @@ -522,7 +770,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, @@ -609,6 +857,35 @@ impl fmt::Display for AlterTableOperation { write!(f, "RESUME RECLUSTER")?; Ok(()) } + AlterTableOperation::Refresh => { + write!(f, "REFRESH") + } + AlterTableOperation::Suspend => { + write!(f, "SUSPEND") + } + AlterTableOperation::Resume => { + write!(f, "RESUME") + } + AlterTableOperation::AutoIncrement { equals, value } => { + write!( + f, + "AUTO_INCREMENT {}{}", + if *equals { "= " } else { "" }, + value + ) + } + 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)) + } } } } @@ -623,6 +900,95 @@ impl fmt::Display for AlterIndexOperation { } } +/// An `ALTER TYPE` statement (`Statement::AlterType`) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterType { + pub name: ObjectName, + pub operation: AlterTypeOperation, +} + +/// An [AlterType] 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 AlterTypeOperation { + Rename(AlterTypeRename), + AddValue(AlterTypeAddValue), + RenameValue(AlterTypeRenameValue), +} + +/// See [AlterTypeOperation::Rename] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterTypeRename { + pub new_name: Ident, +} + +/// See [AlterTypeOperation::AddValue] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterTypeAddValue { + pub if_not_exists: bool, + pub value: Ident, + pub position: Option, +} + +/// See [AlterTypeAddValue] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTypeAddValuePosition { + Before(Ident), + After(Ident), +} + +/// See [AlterTypeOperation::RenameValue] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterTypeRenameValue { + pub from: Ident, + pub to: Ident, +} + +impl fmt::Display for AlterTypeOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Rename(AlterTypeRename { new_name }) => { + write!(f, "RENAME TO {new_name}") + } + Self::AddValue(AlterTypeAddValue { + if_not_exists, + value, + position, + }) => { + write!(f, "ADD VALUE")?; + if *if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " {value}")?; + match position { + Some(AlterTypeAddValuePosition::Before(neighbor_value)) => { + write!(f, " BEFORE {neighbor_value}")?; + } + Some(AlterTypeAddValuePosition::After(neighbor_value)) => { + write!(f, " AFTER {neighbor_value}")?; + } + None => {} + }; + Ok(()) + } + Self::RenameValue(AlterTypeRenameValue { from, to }) => { + write!(f, "RENAME VALUE {from} TO {to}") + } + } + } +} + /// An `ALTER COLUMN` (`Statement::AlterTable`) operation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -641,7 +1007,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. @@ -659,15 +1028,22 @@ impl fmt::Display for AlterColumnOperation { AlterColumnOperation::SetDefault { value } => { write!(f, "SET DEFAULT {value}") } - AlterColumnOperation::DropDefault {} => { + 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, @@ -695,295 +1071,32 @@ impl fmt::Display for AlterColumnOperation { } } -/// A table-level constraint, specified in a `CREATE TABLE` or an -/// `ALTER TABLE ADD ` statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +/// Representation whether a definition can can contains the KEY or INDEX keywords with the same +/// meaning. +/// +/// This enum initially is directed to `FULLTEXT`,`SPATIAL`, and `UNIQUE` indexes on create table +/// statements of `MySQL` [(1)]. +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.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 TableConstraint { - /// MySQL [definition][1] for `UNIQUE` constraints statements:\ - /// * `[CONSTRAINT []] UNIQUE [] [index_type] () ` - /// - /// where: - /// * [index_type][2] is `USING {BTREE | HASH}` - /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` - /// * [index_type_display][4] is `[INDEX | KEY]` - /// - /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html - /// [2]: IndexType - /// [3]: IndexOption - /// [4]: KeyOrIndexDisplay - Unique { - /// Constraint name. - /// - /// Can be not the same as `index_name` - name: Option, - /// Index name - index_name: Option, - /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. - index_type_display: KeyOrIndexDisplay, - /// Optional `USING` of [index type][1] statement before columns. - /// - /// [1]: IndexType - index_type: Option, - /// Identifiers of the columns that are unique. - columns: Vec, - index_options: Vec, - characteristics: Option, - /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` - nulls_distinct: NullsDistinctOption, - }, - /// MySQL [definition][1] for `PRIMARY KEY` constraints statements:\ - /// * `[CONSTRAINT []] PRIMARY KEY [index_name] [index_type] () ` - /// - /// Actually the specification have no `[index_name]` but the next query will complete successfully: - /// ```sql - /// CREATE TABLE unspec_table ( - /// xid INT NOT NULL, - /// CONSTRAINT p_name PRIMARY KEY index_name USING BTREE (xid) - /// ); - /// ``` - /// - /// where: - /// * [index_type][2] is `USING {BTREE | HASH}` - /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` - /// - /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html - /// [2]: IndexType - /// [3]: IndexOption - PrimaryKey { - /// Constraint name. - /// - /// Can be not the same as `index_name` - name: Option, - /// Index name - index_name: Option, - /// Optional `USING` of [index type][1] statement before columns. - /// - /// [1]: IndexType - index_type: Option, - /// Identifiers of the columns that form the primary key. - columns: Vec, - index_options: Vec, - characteristics: Option, - }, - /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () - /// REFERENCES () - /// { [ON DELETE ] [ON UPDATE ] | - /// [ON UPDATE ] [ON DELETE ] - /// }`). - ForeignKey { - name: Option, - columns: Vec, - foreign_table: ObjectName, - referred_columns: Vec, - on_delete: Option, - on_update: Option, - characteristics: Option, - }, - /// `[ CONSTRAINT ] CHECK ()` - Check { - name: Option, - expr: Box, - }, - /// 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. - /// - /// `{INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option]...` - /// - /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html - Index { - /// Whether this index starts with KEY (true) or INDEX (false), to maintain the same syntax. - display_as_key: bool, - /// Index name. - name: Option, - /// Optional [index type][1]. - /// - /// [1]: IndexType - index_type: Option, - /// Referred column identifier list. - columns: 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. - /// - /// Supported syntax: - /// - /// ```markdown - /// {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name] (key_part,...) - /// - /// key_part: col_name - /// ``` - /// - /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html - /// [2]: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html - FulltextOrSpatial { - /// Whether this is a `FULLTEXT` (true) or `SPATIAL` (false) definition. - fulltext: bool, - /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. - index_type_display: KeyOrIndexDisplay, - /// Optional index name. - opt_index_name: Option, - /// Referred column identifier list. - columns: Vec, - }, +pub enum KeyOrIndexDisplay { + /// Nothing to display + None, + /// Display the KEY keyword + Key, + /// Display the INDEX keyword + Index, +} + +impl KeyOrIndexDisplay { + pub fn is_none(self) -> bool { + matches!(self, Self::None) + } } -impl fmt::Display for TableConstraint { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - TableConstraint::Unique { - name, - index_name, - index_type_display, - index_type, - columns, - index_options, - characteristics, - nulls_distinct, - } => { - write!( - f, - "{}UNIQUE{nulls_distinct}{index_type_display:>}{}{} ({})", - display_constraint_name(name), - display_option_spaced(index_name), - display_option(" USING ", "", index_type), - display_comma_separated(columns), - )?; - - if !index_options.is_empty() { - write!(f, " {}", display_separated(index_options, " "))?; - } - - write!(f, "{}", display_option_spaced(characteristics))?; - Ok(()) - } - TableConstraint::PrimaryKey { - name, - index_name, - index_type, - columns, - index_options, - characteristics, - } => { - write!( - f, - "{}PRIMARY KEY{}{} ({})", - display_constraint_name(name), - display_option_spaced(index_name), - display_option(" USING ", "", index_type), - display_comma_separated(columns), - )?; - - if !index_options.is_empty() { - write!(f, " {}", display_separated(index_options, " "))?; - } - - write!(f, "{}", display_option_spaced(characteristics))?; - Ok(()) - } - TableConstraint::ForeignKey { - name, - columns, - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => { - write!( - f, - "{}FOREIGN KEY ({}) REFERENCES {}", - display_constraint_name(name), - display_comma_separated(columns), - foreign_table, - )?; - if !referred_columns.is_empty() { - write!(f, "({})", display_comma_separated(referred_columns))?; - } - if let Some(action) = on_delete { - write!(f, " ON DELETE {action}")?; - } - if let Some(action) = on_update { - write!(f, " ON UPDATE {action}")?; - } - if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; - } - Ok(()) - } - TableConstraint::Check { name, expr } => { - write!(f, "{}CHECK ({})", display_constraint_name(name), expr) - } - TableConstraint::Index { - display_as_key, - name, - index_type, - columns, - } => { - write!(f, "{}", if *display_as_key { "KEY" } else { "INDEX" })?; - if let Some(name) = name { - write!(f, " {name}")?; - } - if let Some(index_type) = index_type { - write!(f, " USING {index_type}")?; - } - write!(f, " ({})", display_comma_separated(columns))?; - - Ok(()) - } - Self::FulltextOrSpatial { - fulltext, - index_type_display, - opt_index_name, - columns, - } => { - if *fulltext { - write!(f, "FULLTEXT")?; - } else { - write!(f, "SPATIAL")?; - } - - write!(f, "{index_type_display:>}")?; - - if let Some(name) = opt_index_name { - write!(f, " {name}")?; - } - - write!(f, " ({})", display_comma_separated(columns))?; - - Ok(()) - } - } - } -} - -/// Representation whether a definition can can contains the KEY or INDEX keywords with the same -/// meaning. -/// -/// This enum initially is directed to `FULLTEXT`,`SPATIAL`, and `UNIQUE` indexes on create table -/// statements of `MySQL` [(1)]. -/// -/// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.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 KeyOrIndexDisplay { - /// Nothing to display - None, - /// Display the KEY keyword - Key, - /// Display the INDEX keyword - Index, -} - -impl KeyOrIndexDisplay { - pub fn is_none(self) -> bool { - matches!(self, Self::None) - } -} - -impl fmt::Display for KeyOrIndexDisplay { +impl fmt::Display for KeyOrIndexDisplay { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let left_space = matches!(f.align(), Some(fmt::Alignment::Right)); @@ -1013,13 +1126,20 @@ impl fmt::Display for KeyOrIndexDisplay { /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html /// [2]: https://dev.mysql.com/doc/refman/8.0/en/create-index.html /// [3]: https://www.postgresql.org/docs/14/sql-createindex.html -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum IndexType { BTree, Hash, - // TODO add Postgresql's possible indexes + GIN, + GiST, + SPGiST, + BRIN, + Bloom, + /// Users may define their own index types, which would + /// not be covered by the above variants. + Custom(Ident), } impl fmt::Display for IndexType { @@ -1027,21 +1147,30 @@ impl fmt::Display for IndexType { match self { Self::BTree => write!(f, "BTREE"), Self::Hash => write!(f, "HASH"), + Self::GIN => write!(f, "GIN"), + Self::GiST => write!(f, "GIST"), + Self::SPGiST => write!(f, "SPGIST"), + Self::BRIN => write!(f, "BRIN"), + Self::Bloom => write!(f, "BLOOM"), + 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), } @@ -1054,9 +1183,9 @@ impl fmt::Display for IndexOption { } } -/// [Postgres] unique index nulls handling option: `[ NULLS [ NOT ] DISTINCT ]` +/// [PostgreSQL] unique index nulls handling option: `[ NULLS [ NOT ] DISTINCT ]` /// -/// [Postgres]: https://www.postgresql.org/docs/17/sql-altertable.html +/// [PostgreSQL]: https://www.postgresql.org/docs/17/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))] @@ -1085,11 +1214,23 @@ impl fmt::Display for NullsDistinctOption { pub struct ProcedureParam { pub name: Ident, pub data_type: DataType, + pub mode: Option, + pub default: 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 { + if let Some(default) = &self.default { + write!(f, "{mode} {} {} = {}", self.name, self.data_type, default) + } else { + write!(f, "{mode} {} {}", self.name, self.data_type) + } + } else if let Some(default) = &self.default { + write!(f, "{} {} = {}", self.name, self.data_type, default) + } else { + write!(f, "{} {}", self.name, self.data_type) + } } } @@ -1100,7 +1241,6 @@ impl fmt::Display for ProcedureParam { pub struct ColumnDef { pub name: Ident, pub data_type: DataType, - pub collation: Option, pub options: Vec, } @@ -1111,9 +1251,6 @@ impl fmt::Display for ColumnDef { } else { write!(f, "{} {}", self.name, self.data_type)?; } - if let Some(collation) = &self.collation { - write!(f, " COLLATE {collation}")?; - } for option in &self.options { write!(f, " {option}")?; } @@ -1143,17 +1280,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(()) } @@ -1373,7 +1534,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>, } @@ -1434,32 +1595,26 @@ pub enum ColumnOption { /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/create/table#default_values) Alias(Expr), - /// `{ PRIMARY KEY | UNIQUE } []` - Unique { - is_primary: bool, - characteristics: Option, - }, - /// A referential integrity constraint (`[FOREIGN KEY REFERENCES - /// () + /// `PRIMARY KEY []` + PrimaryKey(PrimaryKeyConstraint), + /// `UNIQUE []` + Unique(UniqueConstraint), + /// A referential integrity constraint (`REFERENCES () + /// [ MATCH { FULL | PARTIAL | SIMPLE } ] /// { [ON DELETE ] [ON UPDATE ] | /// [ON UPDATE ] [ON DELETE ] - /// } + /// } /// [] /// `). - ForeignKey { - foreign_table: ObjectName, - referred_columns: Vec, - on_delete: Option, - on_update: Option, - characteristics: Option, - }, + ForeignKey(ForeignKeyConstraint), /// `CHECK ()` - Check(Expr), + Check(CheckConstraint), /// Dialect-specific options, such as: /// - MySQL's `AUTO_INCREMENT` or SQLite's `AUTOINCREMENT` /// - ... DialectSpecific(Vec), CharacterSet(ObjectName), + Collation(ObjectName), Comment(String), OnUpdate(Expr), /// `Generated`s are modifiers that follow a column definition in a `CREATE @@ -1506,6 +1661,43 @@ 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), + /// MySQL specific: Column is invisible via SELECT * + /// Syntax: + /// ```sql + /// CREATE TABLE t (foo INT, bar INT INVISIBLE); + /// ``` + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/invisible-columns.html + Invisible, +} + +impl From for ColumnOption { + fn from(c: UniqueConstraint) -> Self { + ColumnOption::Unique(c) + } +} + +impl From for ColumnOption { + fn from(c: PrimaryKeyConstraint) -> Self { + ColumnOption::PrimaryKey(c) + } +} + +impl From for ColumnOption { + fn from(c: CheckConstraint) -> Self { + ColumnOption::Check(c) + } +} +impl From for ColumnOption { + fn from(fk: ForeignKeyConstraint) -> Self { + ColumnOption::ForeignKey(fk) + } } impl fmt::Display for ColumnOption { @@ -1524,41 +1716,47 @@ impl fmt::Display for ColumnOption { } } Alias(expr) => write!(f, "ALIAS {expr}"), - Unique { - is_primary, - characteristics, - } => { - write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" })?; - if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + PrimaryKey(constraint) => { + write!(f, "PRIMARY KEY")?; + if let Some(characteristics) = &constraint.characteristics { + write!(f, " {characteristics}")?; } Ok(()) } - ForeignKey { - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => { - write!(f, "REFERENCES {foreign_table}")?; - if !referred_columns.is_empty() { - write!(f, " ({})", display_comma_separated(referred_columns))?; + Unique(constraint) => { + write!(f, "UNIQUE")?; + if let Some(characteristics) = &constraint.characteristics { + write!(f, " {characteristics}")?; + } + Ok(()) + } + ForeignKey(constraint) => { + write!(f, "REFERENCES {}", constraint.foreign_table)?; + if !constraint.referred_columns.is_empty() { + write!( + f, + " ({})", + display_comma_separated(&constraint.referred_columns) + )?; } - if let Some(action) = on_delete { + if let Some(match_kind) = &constraint.match_kind { + write!(f, " {match_kind}")?; + } + if let Some(action) = &constraint.on_delete { write!(f, " ON DELETE {action}")?; } - if let Some(action) = on_update { + if let Some(action) = &constraint.on_update { write!(f, " ON UPDATE {action}")?; } - if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + if let Some(characteristics) = &constraint.characteristics { + write!(f, " {characteristics}")?; } Ok(()) } - Check(expr) => write!(f, "CHECK ({expr})"), + Check(constraint) => write!(f, "{constraint}"), DialectSpecific(val) => write!(f, "{}", display_separated(val, " ")), CharacterSet(n) => write!(f, "CHARACTER SET {n}"), + Collation(n) => write!(f, "COLLATE {n}"), Comment(v) => write!(f, "COMMENT '{}'", escape_single_quote_string(v)), OnUpdate(expr) => write!(f, "ON UPDATE {expr}"), Generated { @@ -1611,7 +1809,7 @@ impl fmt::Display for ColumnOption { write!(f, "{parameters}") } OnConflict(keyword) => { - write!(f, "ON CONFLICT {:?}", keyword)?; + write!(f, "ON CONFLICT {keyword:?}")?; Ok(()) } Policy(parameters) => { @@ -1620,6 +1818,12 @@ impl fmt::Display for ColumnOption { Tags(tags) => { write!(f, "{tags}") } + Srid(srid) => { + write!(f, "SRID {srid}") + } + Invisible => { + write!(f, "INVISIBLE") + } } } } @@ -1646,7 +1850,7 @@ pub enum GeneratedExpressionMode { } #[must_use] -fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { +pub(crate) fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { struct ConstraintName<'a>(&'a Option); impl fmt::Display for ConstraintName<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -1663,7 +1867,7 @@ fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { /// * `Some(inner)` => create display struct for `"{prefix}{inner}{postfix}"` /// * `_` => do nothing #[must_use] -fn display_option<'a, T: fmt::Display>( +pub(crate) fn display_option<'a, T: fmt::Display>( prefix: &'a str, postfix: &'a str, option: &'a Option, @@ -1685,7 +1889,7 @@ fn display_option<'a, T: fmt::Display>( /// * `Some(inner)` => create display struct for `" {inner}"` /// * `_` => do nothing #[must_use] -fn display_option_spaced(option: &Option) -> impl fmt::Display + '_ { +pub(crate) fn display_option_spaced(option: &Option) -> impl fmt::Display + '_ { display_option(" ", "", option) } @@ -1933,125 +2137,1513 @@ 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 CreateFunction { - pub or_replace: bool, - pub temporary: bool, +pub struct CreateIndex { + /// index name + pub name: Option, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + /// Index type used in the statement. Can also be found inside [`CreateIndex::index_options`] + /// depending on the position of the option within the statement. + pub using: Option, + pub columns: Vec, + pub unique: bool, + pub concurrently: bool, pub if_not_exists: bool, - pub name: ObjectName, - pub args: Option>, - pub return_type: Option, - /// The expression that defines the function. - /// - /// Examples: - /// ```sql - /// AS ((SELECT 1)) - /// AS "console.log();" - /// ``` - pub function_body: Option, - /// Behavior attribute for the function - /// - /// IMMUTABLE | STABLE | VOLATILE - /// - /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) - pub behavior: Option, - /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT - /// - /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) - pub called_on_null: Option, - /// PARALLEL { UNSAFE | RESTRICTED | SAFE } - /// - /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) - pub parallel: Option, - /// USING ... (Hive only) - pub using: Option, - /// Language used in a UDF definition. - /// - /// Example: - /// ```sql - /// CREATE FUNCTION foo() LANGUAGE js AS "console.log();" - /// ``` - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_javascript_udf) - pub language: Option, - /// Determinism keyword used for non-sql UDF definitions. - /// - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) - pub determinism_specifier: Option, - /// List of options for creating the function. + 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`: /// - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) - pub options: Option>, - /// Connection resource for a remote function. + /// - `ALGORITHM` + /// - `LOCK` /// - /// Example: - /// ```sql - /// CREATE FUNCTION foo() - /// RETURNS FLOAT64 - /// REMOTE WITH CONNECTION us.myconnection - /// ``` - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_remote_function) - pub remote_connection: Option, + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/create-index.html + pub alter_options: Vec, } -impl fmt::Display for CreateFunction { +impl fmt::Display for CreateIndex { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "CREATE {or_replace}{temp}FUNCTION {if_not_exists}{name}", - name = self.name, - temp = if self.temporary { "TEMPORARY " } else { "" }, - or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + "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(args) = &self.args { - write!(f, "({})", display_comma_separated(args))?; + if let Some(value) = &self.name { + write!(f, "{value} ")?; } - if let Some(return_type) = &self.return_type { - write!(f, " RETURNS {return_type}")?; + write!(f, "ON {}", self.table_name)?; + if let Some(value) = &self.using { + write!(f, " USING {value} ")?; } - if let Some(determinism_specifier) = &self.determinism_specifier { - write!(f, " {determinism_specifier}")?; + write!(f, "({})", display_comma_separated(&self.columns))?; + if !self.include.is_empty() { + write!(f, " INCLUDE ({})", display_comma_separated(&self.include))?; } - if let Some(language) = &self.language { - write!(f, " LANGUAGE {language}")?; + if let Some(value) = self.nulls_distinct { + if value { + write!(f, " NULLS DISTINCT")?; + } else { + write!(f, " NULLS NOT DISTINCT")?; + } } - if let Some(behavior) = &self.behavior { - write!(f, " {behavior}")?; + if !self.with.is_empty() { + write!(f, " WITH ({})", display_comma_separated(&self.with))?; } - if let Some(called_on_null) = &self.called_on_null { - write!(f, " {called_on_null}")?; + if let Some(predicate) = &self.predicate { + write!(f, " WHERE {predicate}")?; } - if let Some(parallel) = &self.parallel { - write!(f, " {parallel}")?; + if !self.index_options.is_empty() { + write!(f, " {}", display_separated(&self.index_options, " "))?; } - if let Some(remote_connection) = &self.remote_connection { - write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; + if !self.alter_options.is_empty() { + write!(f, " {}", display_separated(&self.alter_options, " "))?; } - if let Some(CreateFunctionBody::AsBeforeOptions(function_body)) = &self.function_body { - write!(f, " AS {function_body}")?; + 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 let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { - write!(f, " RETURN {function_body}")?; + 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})")?; } - if let Some(using) = &self.using { - write!(f, " {using}")?; + + // 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}'")?; } - if let Some(options) = &self.options { - write!( - f, - " OPTIONS({})", - display_comma_separated(options.as_slice()) - )?; + + // Only for SQLite + if self.without_rowid { + write!(f, " WITHOUT ROWID")?; } - if let Some(CreateFunctionBody::AsAfterOptions(function_body)) = &self.function_body { - write!(f, " AS {function_body}")?; + + if let Some(CreateTableLikeKind::Plain(like)) = &self.like { + write!(f, " {like}")?; } - Ok(()) + + 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))] +pub struct CreateFunction { + /// True if this is a `CREATE OR ALTER FUNCTION` statement + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#or-alter) + pub or_alter: bool, + pub or_replace: bool, + pub temporary: bool, + pub if_not_exists: bool, + pub name: ObjectName, + pub args: Option>, + pub return_type: Option, + /// The expression that defines the function. + /// + /// Examples: + /// ```sql + /// AS ((SELECT 1)) + /// AS "console.log();" + /// ``` + pub function_body: Option, + /// Behavior attribute for the function + /// + /// IMMUTABLE | STABLE | VOLATILE + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) + pub behavior: Option, + /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) + pub called_on_null: Option, + /// PARALLEL { UNSAFE | RESTRICTED | SAFE } + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) + pub parallel: Option, + /// USING ... (Hive only) + pub using: Option, + /// Language used in a UDF definition. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION foo() LANGUAGE js AS "console.log();" + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_javascript_udf) + pub language: Option, + /// Determinism keyword used for non-sql UDF definitions. + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) + pub determinism_specifier: Option, + /// List of options for creating the function. + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) + pub options: Option>, + /// Connection resource for a remote function. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION foo() + /// RETURNS FLOAT64 + /// REMOTE WITH CONNECTION us.myconnection + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_remote_function) + pub remote_connection: Option, +} + +impl fmt::Display for CreateFunction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE {or_alter}{or_replace}{temp}FUNCTION {if_not_exists}{name}", + name = self.name, + temp = if self.temporary { "TEMPORARY " } else { "" }, + or_alter = if self.or_alter { "OR ALTER " } else { "" }, + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + )?; + if let Some(args) = &self.args { + write!(f, "({})", display_comma_separated(args))?; + } + if let Some(return_type) = &self.return_type { + write!(f, " RETURNS {return_type}")?; + } + if let Some(determinism_specifier) = &self.determinism_specifier { + write!(f, " {determinism_specifier}")?; + } + if let Some(language) = &self.language { + write!(f, " LANGUAGE {language}")?; + } + if let Some(behavior) = &self.behavior { + write!(f, " {behavior}")?; + } + if let Some(called_on_null) = &self.called_on_null { + write!(f, " {called_on_null}")?; + } + if let Some(parallel) = &self.parallel { + write!(f, " {parallel}")?; + } + if let Some(remote_connection) = &self.remote_connection { + write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; + } + if let Some(CreateFunctionBody::AsBeforeOptions(function_body)) = &self.function_body { + write!(f, " AS {function_body}")?; + } + 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}")?; + } + if let Some(options) = &self.options { + write!( + f, + " OPTIONS({})", + display_comma_separated(options.as_slice()) + )?; + } + if let Some(CreateFunctionBody::AsAfterOptions(function_body)) = &self.function_body { + write!(f, " AS {function_body}")?; + } + if let Some(CreateFunctionBody::AsBeginEnd(bes)) = &self.function_body { + write!(f, " AS {bes}")?; + } + Ok(()) + } +} + +/// ```sql +/// CREATE CONNECTOR [IF NOT EXISTS] connector_name +/// [TYPE datasource_type] +/// [URL datasource_url] +/// [COMMENT connector_comment] +/// [WITH DCPROPERTIES(property_name=property_value, ...)] +/// ``` +/// +/// [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-CreateDataConnectorCreateConnector) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateConnector { + pub name: Ident, + pub if_not_exists: bool, + pub connector_type: Option, + pub url: Option, + pub comment: Option, + pub with_dcproperties: Option>, +} + +impl fmt::Display for CreateConnector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE CONNECTOR {if_not_exists}{name}", + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + name = self.name, + )?; + + if let Some(connector_type) = &self.connector_type { + write!(f, " TYPE '{connector_type}'")?; + } + + if let Some(url) = &self.url { + write!(f, " URL '{url}'")?; + } + + if let Some(comment) = &self.comment { + write!(f, " COMMENT = '{comment}'")?; + } + + if let Some(with_dcproperties) = &self.with_dcproperties { + write!( + f, + " WITH DCPROPERTIES({})", + display_comma_separated(with_dcproperties) + )?; + } + + 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) +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterschema.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 AlterSchemaOperation { + SetDefaultCollate { + collate: Expr, + }, + AddReplica { + replica: Ident, + options: Option>, + }, + DropReplica { + replica: Ident, + }, + SetOptionsParens { + options: Vec, + }, + Rename { + name: ObjectName, + }, + OwnerTo { + owner: Owner, + }, +} + +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)) + } + AlterSchemaOperation::Rename { name } => write!(f, "RENAME TO {name}"), + AlterSchemaOperation::OwnerTo { owner } => write!(f, "OWNER TO {owner}"), + } + } +} +/// `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(), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// Whether the syntax used for the trigger object (ROW or STATEMENT) is `FOR` or `FOR EACH`. +pub enum TriggerObjectKind { + /// The `FOR` syntax is used. + For(TriggerObject), + /// The `FOR EACH` syntax is used. + ForEach(TriggerObject), +} + +impl Display for TriggerObjectKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TriggerObjectKind::For(obj) => write!(f, "FOR {obj}"), + TriggerObjectKind::ForEach(obj) => write!(f, "FOR EACH {obj}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// CREATE TRIGGER +/// +/// Examples: +/// +/// ```sql +/// CREATE TRIGGER trigger_name +/// BEFORE INSERT ON table_name +/// FOR EACH ROW +/// EXECUTE FUNCTION trigger_function(); +/// ``` +/// +/// Postgres: +/// SQL Server: +pub struct 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) + pub or_alter: bool, + /// True if this is a temporary trigger. + /// + /// Examples: + /// + /// ```sql + /// CREATE TEMP TRIGGER trigger_name + /// ``` + /// + /// or + /// + /// ```sql + /// CREATE TEMPORARY TRIGGER trigger_name; + /// CREATE TEMP TRIGGER trigger_name; + /// ``` + /// + /// [SQLite](https://sqlite.org/lang_createtrigger.html#temp_triggers_on_non_temp_tables) + pub temporary: bool, + /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. + /// + /// Example: + /// ```sql + /// CREATE OR REPLACE TRIGGER trigger_name + /// AFTER INSERT ON table_name + /// FOR EACH ROW + /// EXECUTE FUNCTION trigger_function(); + /// ``` + pub or_replace: bool, + /// The `CONSTRAINT` keyword is used to create a trigger as a constraint. + pub is_constraint: bool, + /// The name of the trigger to be created. + pub name: ObjectName, + /// Determines whether the function is called before, after, or instead of the event. + /// + /// Example of BEFORE: + /// + /// ```sql + /// CREATE TRIGGER trigger_name + /// BEFORE INSERT ON table_name + /// FOR EACH ROW + /// EXECUTE FUNCTION trigger_function(); + /// ``` + /// + /// Example of AFTER: + /// + /// ```sql + /// CREATE TRIGGER trigger_name + /// AFTER INSERT ON table_name + /// FOR EACH ROW + /// EXECUTE FUNCTION trigger_function(); + /// ``` + /// + /// Example of INSTEAD OF: + /// + /// ```sql + /// CREATE TRIGGER trigger_name + /// INSTEAD OF INSERT ON table_name + /// FOR EACH ROW + /// EXECUTE FUNCTION trigger_function(); + /// ``` + pub period: Option, + /// Whether the trigger period was specified before the target table name. + /// This does not refer to whether the period is BEFORE, AFTER, or INSTEAD OF, + /// but rather the position of the period clause in relation to the 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 ...; + /// ``` + pub period_before_table: bool, + /// Multiple events can be specified using OR, such as `INSERT`, `UPDATE`, `DELETE`, or `TRUNCATE`. + pub events: Vec, + /// The table on which the trigger is to be created. + pub table_name: ObjectName, + /// The optional referenced table name that can be referenced via + /// the `FROM` keyword. + pub referenced_table_name: Option, + /// This keyword immediately precedes the declaration of one or two relation names that provide access to the transition relations of the triggering statement. + pub referencing: Vec, + /// This specifies whether the trigger function should be fired once for + /// every row affected by the trigger event, or just once per SQL statement. + /// This is optional in some SQL dialects, such as SQLite, and if not specified, in + /// those cases, the implied default is `FOR EACH ROW`. + pub trigger_object: Option, + /// Triggering conditions + pub condition: Option, + /// Execute logic block + pub exec_body: Option, + /// For MSSQL and dialects where statements are preceded by `AS` + pub statements_as: bool, + /// For SQL dialects with statement(s) for a body + pub statements: Option, + /// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`, + pub characteristics: Option, +} + +impl Display for CreateTrigger { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period_before_table, + period, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body, + statements_as, + statements, + characteristics, + } = self; + write!( + f, + "CREATE {temporary}{or_alter}{or_replace}{is_constraint}TRIGGER {name} ", + temporary = if *temporary { "TEMPORARY " } else { "" }, + or_alter = if *or_alter { "OR ALTER " } else { "" }, + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + is_constraint = if *is_constraint { "CONSTRAINT " } else { "" }, + )?; + + if *period_before_table { + if let Some(p) = period { + write!(f, "{p} ")?; + } + if !events.is_empty() { + write!(f, "{} ", display_separated(events, " OR "))?; + } + write!(f, "ON {table_name}")?; + } else { + write!(f, "ON {table_name} ")?; + if let Some(p) = period { + write!(f, "{p}")?; + } + if !events.is_empty() { + write!(f, " {}", display_separated(events, ", "))?; + } + } + + if let Some(referenced_table_name) = referenced_table_name { + write!(f, " FROM {referenced_table_name}")?; + } + + if let Some(characteristics) = characteristics { + write!(f, " {characteristics}")?; + } + + if !referencing.is_empty() { + write!(f, " REFERENCING {}", display_separated(referencing, " "))?; + } + + if let Some(trigger_object) = trigger_object { + write!(f, " {trigger_object}")?; + } + if let Some(condition) = condition { + write!(f, " WHEN {condition}")?; + } + 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(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// DROP TRIGGER +/// +/// ```sql +/// DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] +/// ``` +/// +pub struct DropTrigger { + /// Whether to include the `IF EXISTS` clause. + pub if_exists: bool, + /// The name of the trigger to be dropped. + pub trigger_name: ObjectName, + /// The name of the table from which the trigger is to be dropped. + pub table_name: Option, + /// `CASCADE` or `RESTRICT` + pub option: Option, +} + +impl fmt::Display for DropTrigger { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let DropTrigger { + if_exists, + trigger_name, + table_name, + option, + } = self; + write!(f, "DROP TRIGGER")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + match &table_name { + Some(table_name) => write!(f, " {trigger_name} ON {table_name}")?, + None => write!(f, " {trigger_name}")?, + }; + if let Some(option) = option { + write!(f, " {option}")?; + } + Ok(()) + } +} + +/// A `TRUNCATE` statement. +/// +/// ```sql +/// TRUNCATE TABLE table_names [PARTITION (partitions)] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE | RESTRICT] [ON CLUSTER cluster_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 Truncate { + /// Table names to truncate + pub table_names: Vec, + /// Optional partition specification + pub partitions: Option>, + /// TABLE - optional keyword + pub table: bool, + /// Postgres-specific option: [ RESTART IDENTITY | CONTINUE IDENTITY ] + pub identity: Option, + /// Postgres-specific option: [ CASCADE | RESTRICT ] + pub cascade: Option, + /// ClickHouse-specific option: [ ON CLUSTER cluster_name ] + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/truncate/) + pub on_cluster: Option, +} + +impl fmt::Display for Truncate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let table = if self.table { "TABLE " } else { "" }; + + write!( + f, + "TRUNCATE {table}{table_names}", + table_names = display_comma_separated(&self.table_names) + )?; + + if let Some(identity) = &self.identity { + match identity { + super::TruncateIdentityOption::Restart => write!(f, " RESTART IDENTITY")?, + super::TruncateIdentityOption::Continue => write!(f, " CONTINUE IDENTITY")?, + } + } + if let Some(cascade) = &self.cascade { + match cascade { + super::CascadeOption::Cascade => write!(f, " CASCADE")?, + super::CascadeOption::Restrict => write!(f, " RESTRICT")?, + } + } + + if let Some(ref parts) = &self.partitions { + if !parts.is_empty() { + write!(f, " PARTITION ({})", display_comma_separated(parts))?; + } + } + if let Some(on_cluster) = &self.on_cluster { + write!(f, " ON CLUSTER {on_cluster}")?; + } + Ok(()) + } +} + +impl Spanned for Truncate { + fn span(&self) -> Span { + Span::union_iter( + self.table_names.iter().map(|i| i.name.span()).chain( + self.partitions + .iter() + .flat_map(|i| i.iter().map(|k| k.span())), + ), + ) + } +} + +/// An `MSCK` statement. +/// +/// ```sql +/// MSCK [REPAIR] TABLE table_name [ADD|DROP|SYNC PARTITIONS] +/// ``` +/// MSCK (Hive) - MetaStore Check command +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Msck { + /// Table name to check + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + /// Whether to repair the table + pub repair: bool, + /// Partition action (ADD, DROP, or SYNC) + pub partition_action: Option, +} + +impl fmt::Display for Msck { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "MSCK {repair}TABLE {table}", + repair = if self.repair { "REPAIR " } else { "" }, + table = self.table_name + )?; + if let Some(pa) = &self.partition_action { + write!(f, " {pa}")?; + } + Ok(()) + } +} + +impl Spanned for Msck { + fn span(&self) -> Span { + self.table_name.span() + } +} + +/// CREATE VIEW 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 CreateView { + /// True if this is a `CREATE OR ALTER VIEW` statement + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql) + pub or_alter: bool, + pub or_replace: bool, + pub materialized: bool, + /// Snowflake: SECURE view modifier + /// + pub secure: bool, + /// View name + pub 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` + /// ``` + pub name_before_not_exists: bool, + pub columns: Vec, + pub query: Box, + pub options: CreateTableOptions, + pub cluster_by: Vec, + /// Snowflake: Views can have comments in Snowflake. + /// + pub comment: Option, + /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause + pub with_no_schema_binding: bool, + /// if true, has SQLite `IF NOT EXISTS` clause + pub if_not_exists: bool, + /// if true, has SQLite `TEMP` or `TEMPORARY` clause + pub temporary: bool, + /// if not None, has Clickhouse `TO` clause, specify the table into which to insert results + /// + pub to: Option, + /// MySQL: Optional parameters for the view algorithm, definer, and security context + pub params: Option, +} + +impl fmt::Display for CreateView { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE {or_alter}{or_replace}", + or_alter = if self.or_alter { "OR ALTER " } else { "" }, + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + )?; + if let Some(ref params) = self.params { + params.fmt(f)?; + } + write!( + f, + "{secure}{materialized}{temporary}VIEW {if_not_and_name}{to}", + if_not_and_name = if self.if_not_exists { + if self.name_before_not_exists { + format!("{} IF NOT EXISTS", self.name) + } else { + format!("IF NOT EXISTS {}", self.name) + } + } else { + format!("{}", self.name) + }, + secure = if self.secure { "SECURE " } else { "" }, + materialized = if self.materialized { + "MATERIALIZED " + } else { + "" + }, + temporary = if self.temporary { "TEMPORARY " } else { "" }, + to = self + .to + .as_ref() + .map(|to| format!(" TO {to}")) + .unwrap_or_default() + )?; + if !self.columns.is_empty() { + write!(f, " ({})", display_comma_separated(&self.columns))?; + } + if matches!(self.options, CreateTableOptions::With(_)) { + write!(f, " {}", self.options)?; + } + if let Some(ref comment) = self.comment { + write!(f, " COMMENT = '{}'", escape_single_quote_string(comment))?; + } + if !self.cluster_by.is_empty() { + write!( + f, + " CLUSTER BY ({})", + display_comma_separated(&self.cluster_by) + )?; + } + if matches!(self.options, CreateTableOptions::Options(_)) { + write!(f, " {}", self.options)?; + } + f.write_str(" AS")?; + SpaceOrNewline.fmt(f)?; + self.query.fmt(f)?; + if self.with_no_schema_binding { + write!(f, " WITH NO SCHEMA BINDING")?; + } + Ok(()) + } +} + +/// CREATE EXTENSION statement +/// Note: this is a PostgreSQL-specific 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 CreateExtension { + pub name: Ident, + pub if_not_exists: bool, + pub cascade: bool, + pub schema: Option, + pub version: Option, +} + +impl fmt::Display for CreateExtension { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE EXTENSION {if_not_exists}{name}", + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + name = self.name + )?; + if self.cascade || self.schema.is_some() || self.version.is_some() { + write!(f, " WITH")?; + + if let Some(name) = &self.schema { + write!(f, " SCHEMA {name}")?; + } + if let Some(version) = &self.version { + write!(f, " VERSION {version}")?; + } + if self.cascade { + write!(f, " CASCADE")?; + } + } + + Ok(()) + } +} + +impl Spanned for CreateExtension { + fn span(&self) -> Span { + Span::empty() + } +} + +/// DROP EXTENSION statement +/// Note: this is a PostgreSQL-specific statement +/// +/// # References +/// +/// PostgreSQL Documentation: +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DropExtension { + pub names: Vec, + pub if_exists: bool, + /// `CASCADE` or `RESTRICT` + pub cascade_or_restrict: Option, +} + +impl fmt::Display for DropExtension { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DROP EXTENSION")?; + if self.if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {}", display_comma_separated(&self.names))?; + if let Some(cascade_or_restrict) = &self.cascade_or_restrict { + write!(f, " {cascade_or_restrict}")?; + } + Ok(()) + } +} + +impl Spanned for DropExtension { + fn span(&self) -> Span { + Span::empty() + } +} + +/// Table type for ALTER TABLE statements. +/// Used to distinguish between regular tables, Iceberg tables, and Dynamic tables. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTableType { + /// Iceberg table type + /// + Iceberg, + /// Dynamic table type + /// + Dynamic, +} + +/// ALTER 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 AlterTable { + /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub name: ObjectName, + pub if_exists: bool, + pub only: bool, + pub operations: Vec, + pub location: Option, + /// ClickHouse dialect supports `ON CLUSTER` clause for ALTER TABLE + /// 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) + pub on_cluster: Option, + /// Table type: None for regular tables, Some(AlterTableType) for Iceberg or Dynamic tables + pub table_type: Option, + /// Token that represents the end of the statement (semicolon or EOF) + pub end_token: AttachedToken, +} + +impl fmt::Display for AlterTable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.table_type { + Some(AlterTableType::Iceberg) => write!(f, "ALTER ICEBERG TABLE ")?, + Some(AlterTableType::Dynamic) => write!(f, "ALTER DYNAMIC TABLE ")?, + None => write!(f, "ALTER TABLE ")?, + } + + if self.if_exists { + write!(f, "IF EXISTS ")?; + } + if self.only { + write!(f, "ONLY ")?; + } + write!(f, "{} ", &self.name)?; + if let Some(cluster) = &self.on_cluster { + write!(f, "ON CLUSTER {cluster} ")?; + } + write!(f, "{}", display_comma_separated(&self.operations))?; + if let Some(loc) = &self.location { + write!(f, " {loc}")? + } + Ok(()) + } +} + +/// DROP FUNCTION 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 DropFunction { + pub if_exists: bool, + /// One or more functions to drop + pub func_desc: Vec, + /// `CASCADE` or `RESTRICT` + pub drop_behavior: Option, +} + +impl fmt::Display for DropFunction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "DROP FUNCTION{} {}", + if self.if_exists { " IF EXISTS" } else { "" }, + display_comma_separated(&self.func_desc), + )?; + if let Some(op) = &self.drop_behavior { + write!(f, " {op}")?; + } + Ok(()) + } +} + +impl Spanned for DropFunction { + fn span(&self) -> Span { + Span::empty() } } diff --git a/src/ast/dml.rs b/src/ast/dml.rs index de555c109..c0bfcb19f 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -16,12 +16,7 @@ // under the License. #[cfg(not(feature = "std"))] -use alloc::{ - boxed::Box, - format, - string::{String, ToString}, - vec::Vec, -}; +use alloc::{boxed::Box, format, string::ToString, vec::Vec}; use core::fmt::{self, Display}; #[cfg(feature = "serde")] @@ -29,435 +24,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, InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, OnInsert, - OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, Setting, SqlOption, - SqliteOnConflict, 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, UpdateTableFromKind, }; -/// 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, - /// 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>, - /// 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>, -} - -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}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 { "" }, - 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(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 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))] @@ -547,28 +121,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")?; } @@ -588,7 +166,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(()) } @@ -617,32 +197,108 @@ 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 { + SpaceOrNewline.fmt(f)?; + f.write_str("LIMIT")?; + SpaceOrNewline.fmt(f)?; + Indent(limit).fmt(f)?; + } + Ok(()) + } +} + +/// UPDATE 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 Update { + /// TABLE + pub table: TableWithJoins, + /// Column assignments + pub assignments: Vec, + /// Table which provide value to be set + pub from: Option, + /// WHERE + pub selection: Option, + /// RETURNING + pub returning: Option>, + /// SQLite-specific conflict resolution clause + pub or: Option, + /// LIMIT + pub limit: Option, +} + +impl Display for Update { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("UPDATE ")?; + if let Some(or) = &self.or { + or.fmt(f)?; + f.write_str(" ")?; + } + self.table.fmt(f)?; + if let Some(UpdateTableFromKind::BeforeSet(from)) = &self.from { + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; + } + if !self.assignments.is_empty() { + SpaceOrNewline.fmt(f)?; + f.write_str("SET")?; + indented_list(f, &self.assignments)?; + } + if let Some(UpdateTableFromKind::AfterSet(from)) = &self.from { + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; + } + if let Some(selection) = &self.selection { + SpaceOrNewline.fmt(f)?; + f.write_str("WHERE")?; + SpaceOrNewline.fmt(f)?; + Indent(selection).fmt(f)?; + } + if let Some(returning) = &self.returning { + SpaceOrNewline.fmt(f)?; + f.write_str("RETURNING")?; + indented_list(f, returning)?; } if let Some(limit) = &self.limit { - write!(f, " LIMIT {limit}")?; + SpaceOrNewline.fmt(f)?; + write!(f, "LIMIT {limit}")?; } Ok(()) } diff --git a/src/ast/helpers/key_value_options.rs b/src/ast/helpers/key_value_options.rs new file mode 100644 index 000000000..745c3a65a --- /dev/null +++ b/src/ast/helpers/key_value_options.rs @@ -0,0 +1,102 @@ +// 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. + +//! Key-value options for SQL statements. +//! See [this page](https://docs.snowflake.com/en/sql-reference/commands-data-loading) for more details. + +#[cfg(not(feature = "std"))] +use alloc::{boxed::Box, string::String, vec::Vec}; +use core::fmt; +use core::fmt::Formatter; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +use crate::ast::{display_comma_separated, display_separated, Value}; + +#[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)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct KeyValueOption { + pub option_name: String, + pub option_value: KeyValueOptionKind, +} + +/// An option can have a single value, multiple values or a nested list of values. +/// +/// A value can be numeric, boolean, etc. Enum-style values are represented +/// as Value::Placeholder. For example: MFA_METHOD=SMS will be represented as +/// `Value::Placeholder("SMS".to_string)`. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum KeyValueOptionKind { + Single(Value), + Multi(Vec), + KeyValueOptions(Box), +} + +impl fmt::Display for KeyValueOptions { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let sep = match self.delimiter { + KeyValueOptionsDelimiter::Space => " ", + KeyValueOptionsDelimiter::Comma => ", ", + }; + write!(f, "{}", display_separated(&self.options, sep)) + } +} + +impl fmt::Display for KeyValueOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.option_value { + KeyValueOptionKind::Single(value) => { + write!(f, "{}={value}", self.option_name)?; + } + KeyValueOptionKind::Multi(values) => { + write!( + f, + "{}=({})", + self.option_name, + display_comma_separated(values) + )?; + } + KeyValueOptionKind::KeyValueOptions(options) => { + write!(f, "{}=({options})", self.option_name)?; + } + } + Ok(()) + } +} diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs index a96bffc51..3efbcf7b0 100644 --- a/src/ast/helpers/mod.rs +++ b/src/ast/helpers/mod.rs @@ -15,5 +15,7 @@ // specific language governing permissions and limitations // 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..58a7b0906 --- /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::{format, string::String, 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 a3be57986..fe950c909 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, - 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]). @@ -42,12 +43,11 @@ use crate::parser::ParserError; /// ```rust /// use sqlparser::ast::helpers::stmt_create_table::CreateTableBuilder; /// use sqlparser::ast::{ColumnDef, DataType, Ident, ObjectName}; -/// let builder = CreateTableBuilder::new(ObjectName(vec![Ident::new("table_name")])) +/// let builder = CreateTableBuilder::new(ObjectName::from(vec![Ident::new("table_name")])) /// .if_not_exists(true) /// .columns(vec![ColumnDef { /// name: Ident::new("c1"), /// data_type: DataType::Int(None), -/// collation: None, /// options: vec![], /// }]); /// // You can access internal elements with ease @@ -71,32 +71,29 @@ pub struct CreateTableBuilder { pub if_not_exists: bool, 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, pub enable_schema_evolution: Option, @@ -107,6 +104,17 @@ pub struct CreateTableBuilder { pub with_aggregation_policy: Option, pub with_row_access_policy: Option, pub with_tags: Option>, + pub base_location: Option, + pub external_volume: Option, + 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 { @@ -119,24 +127,21 @@ impl CreateTableBuilder { if_not_exists: false, 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, @@ -144,7 +149,7 @@ impl CreateTableBuilder { partition_by: None, cluster_by: None, clustered_by: None, - options: None, + inherits: None, strict: false, copy_grants: false, enable_schema_evolution: None, @@ -155,6 +160,17 @@ impl CreateTableBuilder { with_aggregation_policy: None, with_row_access_policy: None, with_tags: None, + base_location: None, + external_volume: None, + 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 { @@ -192,6 +208,16 @@ impl CreateTableBuilder { self } + pub fn iceberg(mut self, iceberg: bool) -> Self { + self.iceberg = iceberg; + 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 @@ -212,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 @@ -239,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 } @@ -250,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 @@ -300,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 } @@ -310,8 +312,8 @@ impl CreateTableBuilder { self } - pub fn options(mut self, options: Option>) -> Self { - self.options = options; + pub fn inherits(mut self, inherits: Option>) -> Self { + self.inherits = inherits; self } @@ -371,8 +373,66 @@ impl CreateTableBuilder { self } + pub fn base_location(mut self, base_location: Option) -> Self { + self.base_location = base_location; + 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 catalog_sync(mut self, catalog_sync: Option) -> Self { + self.catalog_sync = catalog_sync; + self + } + + pub fn storage_serialization_policy( + mut self, + storage_serialization_policy: Option, + ) -> Self { + self.storage_serialization_policy = storage_serialization_policy; + 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 { + CreateTable { or_replace: self.or_replace, temporary: self.temporary, external: self.external, @@ -380,24 +440,21 @@ impl CreateTableBuilder { if_not_exists: self.if_not_exists, 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, @@ -405,7 +462,7 @@ 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, enable_schema_evolution: self.enable_schema_evolution, @@ -416,7 +473,19 @@ impl CreateTableBuilder { with_aggregation_policy: self.with_aggregation_policy, with_row_access_policy: self.with_row_access_policy, with_tags: self.with_tags, - }) + base_location: self.base_location, + external_volume: self.external_volume, + 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, + } + .into() } } @@ -435,24 +504,21 @@ impl TryFrom for CreateTableBuilder { if_not_exists, 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, @@ -460,7 +526,7 @@ impl TryFrom for CreateTableBuilder { partition_by, cluster_by, clustered_by, - options, + inherits, strict, copy_grants, enable_schema_evolution, @@ -471,6 +537,17 @@ impl TryFrom for CreateTableBuilder { with_aggregation_policy, with_row_access_policy, with_tags, + base_location, + external_volume, + catalog, + catalog_sync, + storage_serialization_policy, + table_options, + target_lag, + warehouse, + refresh_mode, + initialize, + require_user, }) => Ok(Self { or_replace, temporary, @@ -478,24 +555,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, @@ -503,8 +576,9 @@ impl TryFrom for CreateTableBuilder { partition_by, cluster_by, clustered_by, - options, + inherits, strict, + iceberg, copy_grants, enable_schema_evolution, change_tracking, @@ -515,6 +589,17 @@ impl TryFrom for CreateTableBuilder { with_row_access_policy, with_tags, volatile, + base_location, + external_volume, + 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}" @@ -527,8 +612,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)] @@ -539,7 +625,7 @@ mod tests { #[test] pub fn test_from_valid_statement() { - let builder = CreateTableBuilder::new(ObjectName(vec![Ident::new("table_name")])); + let builder = CreateTableBuilder::new(ObjectName::from(vec![Ident::new("table_name")])); let stmt = builder.clone().build(); diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index 42e1df06b..92a727279 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -21,15 +21,13 @@ #[cfg(not(feature = "std"))] use alloc::string::String; -#[cfg(not(feature = "std"))] -use alloc::vec::Vec; use core::fmt; -use core::fmt::Formatter; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::ast::{Ident, ObjectName}; +use crate::ast::helpers::key_value_options::KeyValueOptions; +use crate::ast::{Ident, ObjectName, SelectItem}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; @@ -38,35 +36,29 @@ use sqlparser_derive::{Visit, VisitMut}; #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct StageParamsObject { pub url: Option, - pub encryption: DataLoadingOptions, + pub encryption: KeyValueOptions, pub endpoint: Option, pub storage_integration: Option, - pub credentials: DataLoadingOptions, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct DataLoadingOptions { - pub options: Vec, + pub credentials: KeyValueOptions, } +/// This enum enables support for both standard SQL select item expressions +/// and Snowflake-specific ones for data loading. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum DataLoadingOptionType { - STRING, - BOOLEAN, - ENUM, +pub enum StageLoadSelectItemKind { + SelectItem(SelectItem), + StageLoadSelectItem(StageLoadSelectItem), } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct DataLoadingOption { - pub option_name: String, - pub option_type: DataLoadingOptionType, - pub value: String, +impl fmt::Display for StageLoadSelectItemKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + StageLoadSelectItemKind::SelectItem(item) => write!(f, "{item}"), + StageLoadSelectItemKind::StageLoadSelectItem(item) => write!(f, "{item}"), + } + } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -105,42 +97,6 @@ impl fmt::Display for StageParamsObject { } } -impl fmt::Display for DataLoadingOptions { - 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(()) - } -} - -impl fmt::Display for DataLoadingOption { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.option_type { - DataLoadingOptionType::STRING => { - write!(f, "{}='{}'", self.option_name, self.value)?; - } - DataLoadingOptionType::ENUM => { - // single quote is omitted - write!(f, "{}={}", self.option_name, self.value)?; - } - DataLoadingOptionType::BOOLEAN => { - // single quote is omitted - write!(f, "{}={}", self.option_name, self.value)?; - } - } - Ok(()) - } -} - impl fmt::Display for StageLoadSelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.alias.is_some() { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2d79f7d6b..9a62b71dc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -21,10 +21,15 @@ use alloc::{ boxed::Box, format, string::{String, ToString}, + vec, vec::Vec, }; -use helpers::{attached_token::AttachedToken, stmt_data_loading::FileStagingCommand}; +use helpers::{ + attached_token::AttachedToken, + stmt_data_loading::{FileStagingCommand, StageLoadSelectItemKind}, +}; +use core::cmp::Ordering; use core::ops::Deref; use core::{ fmt::{self, Display}, @@ -37,43 +42,58 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::tokenizer::Span; +use crate::{ + display_utils::SpaceOrNewline, + tokenizer::{Span, Token}, +}; +use crate::{ + display_utils::{Indent, NewLine}, + keywords::Keyword, +}; pub use self::data_type::{ - ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, EnumMember, ExactNumberInfo, - StructBracketKind, TimezoneInfo, + ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember, + ExactNumberInfo, IntervalFields, StructBracketKind, TimezoneInfo, }; pub use self::dcl::{ - AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, + AlterRoleOperation, CreateRole, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ - AlterColumnOperation, AlterIndexOperation, AlterPolicyOperation, AlterTableOperation, - ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateFunction, Deduplicate, DeferrableInitial, DropBehavior, - GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexOption, - IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, ProcedureParam, - ReferentialAction, TableConstraint, TagsColumnOption, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeRepresentation, ViewColumnDef, + AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, + AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, + AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, + AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, + ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, + ConstraintCharacteristics, CreateConnector, CreateDomain, CreateExtension, CreateFunction, + CreateIndex, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, + DropBehavior, DropExtension, DropFunction, DropTrigger, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, + NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, + ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; -pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; +pub use self::dml::{Delete, Insert, Update}; 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, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, - NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, - OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, + 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, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, - TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableSample, - TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, - TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, - TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, - WildcardAdditionalOptions, With, WithFill, + 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::{ @@ -83,20 +103,27 @@ pub use self::trigger::{ pub use self::value::{ escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString, - TrimWhereField, Value, + NormalizationForm, TrimWhereField, Value, ValueWithSpan, }; -use crate::ast::helpers::stmt_data_loading::{ - DataLoadingOptions, StageLoadSelectItem, StageParamsObject, -}; +use crate::ast::helpers::key_value_options::KeyValueOptions; +use crate::ast::helpers::stmt_data_loading::StageParamsObject; + #[cfg(feature = "visitor")] pub use visitor::*; +pub use self::data_type::GeometricTypeKind; + mod data_type; mod dcl; mod ddl; mod dml; pub mod helpers; +pub mod table_constraints; +pub use table_constraints::{ + CheckConstraint, ForeignKeyConstraint, FullTextOrSpatialConstraint, IndexConstraint, + PrimaryKeyConstraint, TableConstraint, UniqueConstraint, +}; mod operator; mod query; mod spans; @@ -123,30 +150,39 @@ 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(()) } } -pub fn display_separated<'a, T>(slice: &'a [T], sep: &'static str) -> DisplaySeparated<'a, T> +pub(crate) fn display_separated<'a, T>(slice: &'a [T], sep: &'static str) -> DisplaySeparated<'a, T> where T: fmt::Display, { DisplaySeparated { slice, sep } } -pub fn display_comma_separated(slice: &[T]) -> DisplaySeparated<'_, T> +pub(crate) fn display_comma_separated(slice: &[T]) -> DisplaySeparated<'_, T> where T: fmt::Display, { DisplaySeparated { slice, sep: ", " } } +/// Writes the given statements to the formatter, each ending with +/// a semicolon and space separated. +fn format_statement_list(f: &mut fmt::Formatter, statements: &[Statement]) -> fmt::Result { + write!(f, "{}", display_separated(statements, "; "))?; + // We manually insert semicolon for the last statement, + // since display_separated doesn't handle that case. + write!(f, ";") +} + /// 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 { @@ -188,6 +224,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 @@ -267,7 +332,13 @@ impl fmt::Display for Ident { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct ObjectName(pub Vec); +pub struct ObjectName(pub Vec); + +impl From> for ObjectName { + fn from(idents: Vec) -> Self { + ObjectName(idents.into_iter().map(ObjectNamePart::Identifier).collect()) + } +} impl fmt::Display for ObjectName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -275,6 +346,52 @@ impl fmt::Display for ObjectName { } } +/// A single part of an ObjectName +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[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, + } + } +} + +impl fmt::Display for ObjectNamePart { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + 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)] @@ -365,28 +482,36 @@ impl fmt::Display for Interval { /// A field definition within a struct /// -/// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type +/// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_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 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(()) } } } /// A field definition within a union /// -/// [duckdb]: https://duckdb.org/docs/sql/data_types/union.html +/// [DuckDB]: https://duckdb.org/docs/sql/data_types/union.html #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -403,7 +528,7 @@ impl fmt::Display for UnionField { /// A dictionary field within a dictionary. /// -/// [duckdb]: https://duckdb.org/docs/sql/data_types/struct#creating-structs +/// [DuckDB]: https://duckdb.org/docs/sql/data_types/struct#creating-structs #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -434,7 +559,7 @@ impl Display for Map { /// A map field within a map. /// -/// [duckdb]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps +/// [DuckDB]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -532,6 +657,31 @@ pub enum CastKind { DoubleColon, } +/// `MATCH` type for constraint references +/// +/// See: +#[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 ConstraintReferenceMatchKind { + /// `MATCH FULL` + Full, + /// `MATCH PARTIAL` + Partial, + /// `MATCH SIMPLE` + Simple, +} + +impl fmt::Display for ConstraintReferenceMatchKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Full => write!(f, "MATCH FULL"), + Self::Partial => write!(f, "MATCH PARTIAL"), + Self::Simple => write!(f, "MATCH SIMPLE"), + } + } +} + /// `EXTRACT` syntax variants. /// /// In Snowflake dialect, the `EXTRACT` expression can support either the `from` syntax @@ -566,6 +716,27 @@ pub enum CeilFloorKind { Scale(Value), } +/// A WHEN clause in a CASE expression containing both +/// the condition and its corresponding result +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CaseWhen { + pub condition: Expr, + pub result: Expr, +} + +impl fmt::Display for CaseWhen { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("WHEN ")?; + self.condition.fmt(f)?; + f.write_str(" THEN")?; + SpaceOrNewline.fmt(f)?; + Indent(&self.result).fmt(f)?; + Ok(()) + } +} + /// An SQL expression of any type. /// /// # Semantics / Type Checking @@ -601,17 +772,17 @@ pub enum Expr { /// such as maps, arrays, and lists: /// - Array /// - A 1-dim array `a[1]` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]` /// - A 2-dim array `a[1][2]` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]` /// - Map or Struct (Bracket-style) /// - A map `a['field1']` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]` /// - A 2-dim map `a['field1']['field2']` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]` /// - Struct (Dot-style) (only effect when the chain contains both subscript and expr) /// - A struct access `a[field1].field2` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]` /// - If a struct access likes `a.field1.field2`, it will be represented by CompoundIdentifier([a, field1, field2]) CompoundFieldAccess { root: Box, @@ -628,11 +799,6 @@ pub enum Expr { /// The path to the data to extract. path: JsonPath, }, - /// CompositeAccess eg: SELECT foo(bar).z, (information_schema._pg_expandarray(array['i','i'])).n - CompositeAccess { - expr: Box, - key: Ident, - }, /// `IS FALSE` operator IsFalse(Box), /// `IS NOT FALSE` operator @@ -653,6 +819,12 @@ pub enum Expr { IsDistinctFrom(Box, Box), /// `IS NOT DISTINCT FROM` operator IsNotDistinctFrom(Box, Box), + /// ` IS [ NOT ] [ form ] NORMALIZED` + IsNormalized { + expr: Box, + form: Option, + negated: bool, + }, /// `[ NOT ] IN (val1, val2, ...)` InList { expr: Box, @@ -692,7 +864,7 @@ pub enum Expr { any: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// `ILIKE` (case-insensitive `LIKE`) ILike { @@ -702,14 +874,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 { @@ -763,8 +935,9 @@ pub enum Expr { kind: CastKind, expr: Box, data_type: DataType, - // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery - // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax + /// Optional CAST(string_expression AS type FORMAT format_string_expression) as used by [BigQuery] + /// + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax format: Option, }, /// AT a timestamp to a different timezone e.g. `FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00'` @@ -827,6 +1000,10 @@ pub enum Expr { /// true if the expression is represented using the `SUBSTRING(expr, start, len)` syntax /// This flag is used for formatting. special: bool, + + /// true if the expression is represented using the `SUBSTR` shorthand + /// This flag is used for formatting. + shorthand: bool, }, /// ```sql /// TRIM([BOTH | LEADING | TRAILING] [ FROM] ) @@ -857,47 +1034,32 @@ pub enum Expr { /// Nested expression e.g. `(foo > bar)` or `(1)` Nested(Box), /// A literal value, such as string, number, date or NULL - Value(Value), + Value(ValueWithSpan), + /// Prefixed expression, e.g. introducer strings, projection prefix /// - IntroducedString { - introducer: String, - value: Value, + /// + Prefixed { + prefix: Ident, + /// The value of the constant. + /// Hint: you can unwrap the string value using `value.into_string()`. + value: Box, }, /// A constant of form ` 'value'`. /// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`), /// as well as constants of other types (a non-standard PostgreSQL extension). - TypedString { - data_type: DataType, - value: String, - }, + TypedString(TypedString), /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), - /// Arbitrary expr method call - /// - /// Syntax: - /// - /// `.....` - /// - /// > `arbitrary-expr` can be any expression including a function call. - /// - /// Example: - /// - /// ```sql - /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') - /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)') - /// ``` - /// - /// (mssql): - Method(Method), /// `CASE [] WHEN THEN ... [ELSE ] END` /// /// Note we only recognize a complete single expression as ``, /// not `< 0` nor `1, 2, 3` as allowed in a `` per /// Case { + case_token: AttachedToken, + end_token: AttachedToken, operand: Option>, - conditions: Vec, - results: Vec, + conditions: Vec, else_result: Option>, }, /// An exists expression `[ NOT ] EXISTS(SELECT ...)`, used in expressions like @@ -974,7 +1136,7 @@ pub enum Expr { /// [(1)]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match MatchAgainst { /// `(, , ...)`. - columns: Vec, + columns: Vec, /// ``. match_value: Value, /// `` @@ -1008,8 +1170,19 @@ pub enum Expr { /// param -> expr | (param1, ...) -> expr /// ``` /// - /// See . + /// [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/stable/sql/functions/lambda) Lambda(LambdaFunction), + /// Checks membership of a value in a JSON array + MemberOf(MemberOf), +} + +impl Expr { + /// Creates a new [`Expr::Value`] + pub fn value(value: impl Into) -> Self { + Expr::Value(value.into()) + } } /// The contents inside the `[` and `]` in a subscript expression. @@ -1089,8 +1262,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}]"), } } } @@ -1118,7 +1291,7 @@ impl fmt::Display for LambdaFunction { /// `OneOrManyWithParens` implements `Deref` and `IntoIterator`, /// so you can call slice methods on it and iterate over items /// # Examples -/// Acessing as a slice: +/// Accessing as a slice: /// ``` /// # use sqlparser::ast::OneOrManyWithParens; /// let one = OneOrManyWithParens::One("a"); @@ -1292,12 +1465,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(()) } @@ -1365,7 +1538,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 { "" }, @@ -1390,7 +1563,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 { "" }, @@ -1419,6 +1592,24 @@ impl fmt::Display for Expr { if *regexp { "REGEXP" } else { "RLIKE" }, pattern ), + Expr::IsNormalized { + expr, + form, + negated, + } => { + let not_ = if *negated { "NOT " } else { "" }; + if form.is_none() { + write!(f, "{expr} IS {not_}NORMALIZED") + } else { + write!( + f, + "{} IS {}{} NORMALIZED", + expr, + not_, + form.as_ref().unwrap() + ) + } + } Expr::SimilarTo { negated, expr, @@ -1427,7 +1618,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, @@ -1472,7 +1663,15 @@ impl fmt::Display for Expr { Expr::UnaryOp { op, expr } => { if op == &UnaryOperator::PGPostfixFactorial { write!(f, "{expr}{op}") - } else if op == &UnaryOperator::Not { + } else if matches!( + op, + UnaryOperator::Not + | UnaryOperator::Hash + | UnaryOperator::AtDashAt + | UnaryOperator::DoubleAt + | UnaryOperator::QuestionDash + | UnaryOperator::QuestionPipe + ) { write!(f, "{op} {expr}") } else { write!(f, "{op}{expr}") @@ -1562,31 +1761,33 @@ impl fmt::Display for Expr { Expr::Collate { expr, collation } => write!(f, "{expr} COLLATE {collation}"), Expr::Nested(ast) => write!(f, "({ast})"), Expr::Value(v) => write!(f, "{v}"), - Expr::IntroducedString { introducer, value } => write!(f, "{introducer} {value}"), - Expr::TypedString { data_type, value } => { - write!(f, "{data_type}")?; - write!(f, " '{}'", &value::escape_single_quote_string(value)) - } - Expr::Function(fun) => write!(f, "{fun}"), - Expr::Method(method) => write!(f, "{method}"), + Expr::Prefixed { prefix, value } => write!(f, "{prefix} {value}"), + Expr::TypedString(ts) => ts.fmt(f), + Expr::Function(fun) => fun.fmt(f), Expr::Case { + case_token: _, + end_token: _, operand, conditions, - results, else_result, } => { - write!(f, "CASE")?; + f.write_str("CASE")?; if let Some(operand) = operand { - write!(f, " {operand}")?; + f.write_str(" ")?; + operand.fmt(f)?; } - for (c, r) in conditions.iter().zip(results) { - write!(f, " WHEN {c} THEN {r}")?; + for when in conditions { + 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, @@ -1638,8 +1839,13 @@ impl fmt::Display for Expr { substring_from, substring_for, special, + shorthand, } => { - write!(f, "SUBSTRING({expr}")?; + f.write_str("SUBSTR")?; + if !*shorthand { + f.write_str("ING")?; + } + write!(f, "({expr}")?; if let Some(from_part) = substring_from { if *special { write!(f, ", {from_part}")?; @@ -1712,7 +1918,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)) @@ -1726,9 +1932,6 @@ impl fmt::Display for Expr { Expr::JsonAccess { value, path } => { write!(f, "{value}{path}") } - Expr::CompositeAccess { expr, key } => { - write!(f, "{expr}.{key}") - } Expr::AtTimeZone { timestamp, time_zone, @@ -1758,6 +1961,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}"), } } } @@ -1773,8 +1977,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), } } } @@ -1802,14 +2012,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 {}", @@ -1817,12 +2032,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, @@ -2011,140 +2230,480 @@ pub enum Password { NullPassword, } -/// Represents an expression assignment within a variable `DECLARE` statement. +/// A `CASE` statement. /// /// Examples: /// ```sql -/// DECLARE variable_name := 42 -/// DECLARE variable_name DEFAULT 42 +/// CASE +/// WHEN EXISTS(SELECT 1) +/// THEN SELECT 1 FROM T; +/// WHEN EXISTS(SELECT 2) +/// THEN SELECT 1 FROM U; +/// ELSE +/// SELECT 1 FROM V; +/// END CASE; /// ``` +/// +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#case_search_expression) +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/snowflake-scripting/case) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum DeclareAssignment { - /// Plain expression specified. - Expr(Box), +pub struct CaseStatement { + /// The `CASE` token that starts the statement. + pub case_token: AttachedToken, + pub match_expr: Option, + pub when_blocks: Vec, + pub else_block: Option, + /// The last token of the statement (`END` or `CASE`). + pub end_case_token: AttachedToken, +} - /// Expression assigned via the `DEFAULT` keyword - Default(Box), +impl fmt::Display for CaseStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let CaseStatement { + case_token: _, + match_expr, + when_blocks, + else_block, + end_case_token: AttachedToken(end), + } = self; - /// Expression assigned via the `:=` syntax - /// - /// Example: - /// ```sql - /// DECLARE variable_name := 42; - /// ``` - DuckAssignment(Box), + write!(f, "CASE")?; - /// Expression via the `FOR` keyword - /// - /// Example: - /// ```sql - /// DECLARE c1 CURSOR FOR res - /// ``` - For(Box), + if let Some(expr) = match_expr { + write!(f, " {expr}")?; + } - /// Expression via the `=` syntax. - /// - /// Example: - /// ```sql - /// DECLARE @variable AS INT = 100 - /// ``` - MsSqlAssignment(Box), -} + if !when_blocks.is_empty() { + write!(f, " {}", display_separated(when_blocks, " "))?; + } -impl fmt::Display for DeclareAssignment { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - DeclareAssignment::Expr(expr) => { - write!(f, "{expr}") - } - DeclareAssignment::Default(expr) => { - write!(f, "DEFAULT {expr}") - } - DeclareAssignment::DuckAssignment(expr) => { - write!(f, ":= {expr}") - } - DeclareAssignment::MsSqlAssignment(expr) => { - write!(f, "= {expr}") - } - DeclareAssignment::For(expr) => { - write!(f, "FOR {expr}") + if let Some(else_block) = else_block { + write!(f, " {else_block}")?; + } + + write!(f, " END")?; + + if let Token::Word(w) = &end.token { + if w.keyword == Keyword::CASE { + write!(f, " CASE")?; } } + + Ok(()) } } -/// Represents the type of a `DECLARE` statement. +/// An `IF` statement. +/// +/// Example (BigQuery or Snowflake): +/// ```sql +/// IF TRUE THEN +/// SELECT 1; +/// SELECT 2; +/// ELSEIF TRUE THEN +/// SELECT 3; +/// ELSE +/// SELECT 4; +/// END IF +/// ``` +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#if) +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/snowflake-scripting/if) +/// +/// Example (MSSQL): +/// ```sql +/// IF 1=1 SELECT 1 ELSE SELECT 2 +/// ``` +/// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/if-else-transact-sql?view=sql-server-ver16) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum DeclareType { - /// Cursor variable type. e.g. [Snowflake] [Postgres] - /// - /// [Snowflake]: https://docs.snowflake.com/en/developer-guide/snowflake-scripting/cursors#declaring-a-cursor - /// [Postgres]: https://www.postgresql.org/docs/current/plpgsql-cursors.html - Cursor, - - /// Result set variable type. [Snowflake] - /// - /// Syntax: - /// ```text - /// RESULTSET [ { DEFAULT | := } ( ) ] ; - /// ``` - /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/snowflake-scripting/declare#resultset-declaration-syntax - ResultSet, - - /// Exception declaration syntax. [Snowflake] - /// - /// Syntax: - /// ```text - /// EXCEPTION [ ( , '' ) ] ; - /// ``` - /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/snowflake-scripting/declare#exception-declaration-syntax - Exception, +pub struct IfStatement { + pub if_block: ConditionalStatementBlock, + pub elseif_blocks: Vec, + pub else_block: Option, + pub end_token: Option, } -impl fmt::Display for DeclareType { +impl fmt::Display for IfStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - DeclareType::Cursor => { - write!(f, "CURSOR") - } - DeclareType::ResultSet => { - write!(f, "RESULTSET") - } - DeclareType::Exception => { - write!(f, "EXCEPTION") - } + let IfStatement { + if_block, + elseif_blocks, + else_block, + end_token, + } = self; + + write!(f, "{if_block}")?; + + for elseif_block in elseif_blocks { + write!(f, " {elseif_block}")?; + } + + if let Some(else_block) = else_block { + write!(f, " {else_block}")?; } + + if let Some(AttachedToken(end_token)) = end_token { + write!(f, " END {end_token}")?; + } + + Ok(()) } } -/// A `DECLARE` statement. -/// [Postgres] [Snowflake] [BigQuery] +/// A `WHILE` statement. /// -/// Examples: +/// Example: /// ```sql -/// DECLARE variable_name := 42 -/// DECLARE liahona CURSOR FOR SELECT * FROM films; +/// WHILE @@FETCH_STATUS = 0 +/// BEGIN +/// FETCH NEXT FROM c1 INTO @var1, @var2; +/// END /// ``` /// -/// [Postgres]: https://www.postgresql.org/docs/current/sql-declare.html -/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/snowflake-scripting/declare -/// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#declare +/// [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 Declare { - /// The name(s) being declared. - /// Example: `DECLARE a, b, c DEFAULT 42; - pub names: Vec, - /// Data-type assigned to the declared variable. - /// Example: `DECLARE x INT64 DEFAULT 42; - pub data_type: Option, - /// Expression being assigned to the declared variable. - pub assignment: Option, +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 +/// WHEN EXISTS(SELECT 1) THEN SELECT 1; +/// ``` +/// +/// Example 2: +/// ```sql +/// IF TRUE THEN SELECT 1; SELECT 2; +/// ``` +/// +/// Example 3: +/// ```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))] +pub struct ConditionalStatementBlock { + pub start_token: AttachedToken, + pub condition: Option, + pub then_token: Option, + pub conditional_statements: ConditionalStatements, +} + +impl ConditionalStatementBlock { + pub fn statements(&self) -> &Vec { + self.conditional_statements.statements() + } +} + +impl fmt::Display for ConditionalStatementBlock { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let ConditionalStatementBlock { + start_token: AttachedToken(start_token), + condition, + then_token, + conditional_statements, + } = self; + + write!(f, "{start_token}")?; + + if let Some(condition) = condition { + write!(f, " {condition}")?; + } + + if then_token.is_some() { + write!(f, " THEN")?; + } + + if !conditional_statements.statements().is_empty() { + write!(f, " {conditional_statements}")?; + } + + Ok(()) + } +} + +/// A list of statements in a [ConditionalStatementBlock]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ConditionalStatements { + /// SELECT 1; SELECT 2; SELECT 3; ... + Sequence { statements: Vec }, + /// BEGIN SELECT 1; SELECT 2; SELECT 3; ... END + BeginEnd(BeginEndStatements), +} + +impl ConditionalStatements { + pub fn statements(&self) -> &Vec { + match self { + ConditionalStatements::Sequence { statements } => statements, + ConditionalStatements::BeginEnd(bes) => &bes.statements, + } + } +} + +impl fmt::Display for ConditionalStatements { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ConditionalStatements::Sequence { statements } => { + if !statements.is_empty() { + format_statement_list(f, statements)?; + } + Ok(()) + } + ConditionalStatements::BeginEnd(bes) => write!(f, "{bes}"), + } + } +} + +/// Represents a list of statements enclosed within `BEGIN` and `END` keywords. +/// Example: +/// ```sql +/// BEGIN +/// SELECT 1; +/// SELECT 2; +/// END +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct BeginEndStatements { + pub begin_token: AttachedToken, + pub statements: Vec, + pub end_token: AttachedToken, +} + +impl fmt::Display for BeginEndStatements { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + } = self; + + if begin_token.token != Token::EOF { + write!(f, "{begin_token} ")?; + } + if !statements.is_empty() { + format_statement_list(f, statements)?; + } + if end_token.token != Token::EOF { + write!(f, " {end_token}")?; + } + Ok(()) + } +} + +/// A `RAISE` statement. +/// +/// Examples: +/// ```sql +/// RAISE USING MESSAGE = 'error'; +/// +/// RAISE myerror; +/// ``` +/// +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#raise) +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/snowflake-scripting/raise) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct RaiseStatement { + pub value: Option, +} + +impl fmt::Display for RaiseStatement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let RaiseStatement { value } = self; + + write!(f, "RAISE")?; + if let Some(value) = value { + write!(f, " {value}")?; + } + + Ok(()) + } +} + +/// Represents the error value of a [RaiseStatement]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RaiseStatementValue { + /// `RAISE USING MESSAGE = 'error'` + UsingMessage(Expr), + /// `RAISE myerror` + Expr(Expr), +} + +impl fmt::Display for RaiseStatementValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RaiseStatementValue::Expr(expr) => write!(f, "{expr}"), + RaiseStatementValue::UsingMessage(expr) => write!(f, "USING MESSAGE = {expr}"), + } + } +} + +/// Represents an expression assignment within a variable `DECLARE` statement. +/// +/// Examples: +/// ```sql +/// DECLARE variable_name := 42 +/// DECLARE variable_name DEFAULT 42 +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum DeclareAssignment { + /// Plain expression specified. + Expr(Box), + + /// Expression assigned via the `DEFAULT` keyword + Default(Box), + + /// Expression assigned via the `:=` syntax + /// + /// Example: + /// ```sql + /// DECLARE variable_name := 42; + /// ``` + DuckAssignment(Box), + + /// Expression via the `FOR` keyword + /// + /// Example: + /// ```sql + /// DECLARE c1 CURSOR FOR res + /// ``` + For(Box), + + /// Expression via the `=` syntax. + /// + /// Example: + /// ```sql + /// DECLARE @variable AS INT = 100 + /// ``` + MsSqlAssignment(Box), +} + +impl fmt::Display for DeclareAssignment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DeclareAssignment::Expr(expr) => { + write!(f, "{expr}") + } + DeclareAssignment::Default(expr) => { + write!(f, "DEFAULT {expr}") + } + DeclareAssignment::DuckAssignment(expr) => { + write!(f, ":= {expr}") + } + DeclareAssignment::MsSqlAssignment(expr) => { + write!(f, "= {expr}") + } + DeclareAssignment::For(expr) => { + write!(f, "FOR {expr}") + } + } + } +} + +/// Represents the type of a `DECLARE` 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 DeclareType { + /// Cursor variable type. e.g. [Snowflake] [PostgreSQL] [MsSql] + /// + /// [Snowflake]: https://docs.snowflake.com/en/developer-guide/snowflake-scripting/cursors#declaring-a-cursor + /// [PostgreSQL]: https://www.postgresql.org/docs/current/plpgsql-cursors.html + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-cursor-transact-sql + Cursor, + + /// Result set variable type. [Snowflake] + /// + /// Syntax: + /// ```text + /// RESULTSET [ { DEFAULT | := } ( ) ] ; + /// ``` + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/snowflake-scripting/declare#resultset-declaration-syntax + ResultSet, + + /// Exception declaration syntax. [Snowflake] + /// + /// Syntax: + /// ```text + /// EXCEPTION [ ( , '' ) ] ; + /// ``` + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/snowflake-scripting/declare#exception-declaration-syntax + Exception, +} + +impl fmt::Display for DeclareType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DeclareType::Cursor => { + write!(f, "CURSOR") + } + DeclareType::ResultSet => { + write!(f, "RESULTSET") + } + DeclareType::Exception => { + write!(f, "EXCEPTION") + } + } + } +} + +/// A `DECLARE` statement. +/// [PostgreSQL] [Snowflake] [BigQuery] +/// +/// Examples: +/// ```sql +/// DECLARE variable_name := 42 +/// DECLARE liahona CURSOR FOR SELECT * FROM films; +/// ``` +/// +/// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-declare.html +/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/snowflake-scripting/declare +/// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#declare +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Declare { + /// The name(s) being declared. + /// Example: `DECLARE a, b, c DEFAULT 42; + pub names: Vec, + /// Data-type assigned to the declared variable. + /// Example: `DECLARE x INT64 DEFAULT 42; + pub data_type: Option, + /// Expression being assigned to the declared variable. + pub assignment: Option, /// Represents the type of the declared variable. pub declare_type: Option, /// Causes the cursor to return data in binary rather than in text format. @@ -2228,10 +2787,11 @@ impl fmt::Display for Declare { } /// Sql options of a `CREATE TABLE` statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum CreateTableOptions { + #[default] None, /// Options specified using the `WITH` keyword. /// e.g. `WITH (description = "123")` @@ -2252,6 +2812,12 @@ 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 fmt::Display for CreateTableOptions { @@ -2263,6 +2829,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(()), } } @@ -2326,65 +2898,283 @@ pub enum CreatePolicyCommand { Delete, } -/// A top-level statement (SELECT, INSERT, CREATE, etc.) -#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr( - feature = "visitor", - derive(Visit, VisitMut), - visit(with = "visit_statement") -)] -pub enum Statement { - /// ```sql - /// ANALYZE - /// ``` - /// Analyze (Hive) - Analyze { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - partitions: Option>, - for_columns: bool, - columns: Vec, - cache_metadata: bool, - noscan: bool, - compute_statistics: bool, - has_table_keyword: bool, +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum Set { + /// SQL Standard-style + /// SET a = 1; + SingleAssignment { + scope: Option, + hivevar: bool, + variable: ObjectName, + values: Vec, + }, + /// Snowflake-style + /// SET (a, b, ..) = (1, 2, ..); + ParenthesizedAssignments { + variables: Vec, + values: Vec, + }, + /// MySQL-style + /// SET a = 1, b = 2, ..; + MultipleAssignments { assignments: Vec }, + /// Session authorization for Postgres/Redshift + /// + /// ```sql + /// SET SESSION AUTHORIZATION { user_name | DEFAULT } + /// ``` + /// + /// See + /// See + SetSessionAuthorization(SetSessionAuthorizationParam), + /// MS-SQL session + /// + /// See + SetSessionParam(SetSessionParamKind), + /// ```sql + /// SET [ SESSION | LOCAL ] ROLE role_name + /// ``` + /// + /// Sets session state. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4] + /// + /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#set-role-statement + /// [2]: https://www.postgresql.org/docs/14/sql-set-role.html + /// [3]: https://dev.mysql.com/doc/refman/8.0/en/set-role.html + /// [4]: https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10004.htm + SetRole { + /// Non-ANSI optional identifier to inform if the role is defined inside the current session (`SESSION`) or transaction (`LOCAL`). + context_modifier: Option, + /// Role name. If NONE is specified, then the current role name is removed. + role_name: Option, + }, + /// ```sql + /// SET TIME ZONE + /// ``` + /// + /// Note: this is a PostgreSQL-specific statements + /// `SET TIME ZONE ` is an alias for `SET timezone TO ` in PostgreSQL + /// However, we allow it for all dialects. + SetTimeZone { local: bool, value: Expr }, + /// ```sql + /// SET NAMES 'charset_name' [COLLATE 'collation_name'] + /// ``` + SetNames { + charset_name: Ident, + collation_name: Option, + }, + /// ```sql + /// SET NAMES DEFAULT + /// ``` + /// + /// Note: this is a MySQL-specific statement. + SetNamesDefault {}, + /// ```sql + /// SET TRANSACTION ... + /// ``` + SetTransaction { + modes: Vec, + snapshot: Option, + session: bool, }, +} + +impl Display for Set { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ParenthesizedAssignments { variables, values } => write!( + f, + "SET ({}) = ({})", + display_comma_separated(variables), + display_comma_separated(values) + ), + Self::MultipleAssignments { assignments } => { + write!(f, "SET {}", display_comma_separated(assignments)) + } + Self::SetRole { + context_modifier, + role_name, + } => { + let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE")); + write!( + f, + "SET {modifier}ROLE {role_name}", + modifier = context_modifier.map(|m| format!("{m}")).unwrap_or_default() + ) + } + Self::SetSessionAuthorization(kind) => write!(f, "SET SESSION AUTHORIZATION {kind}"), + Self::SetSessionParam(kind) => write!(f, "SET {kind}"), + Self::SetTransaction { + modes, + snapshot, + session, + } => { + if *session { + write!(f, "SET SESSION CHARACTERISTICS AS TRANSACTION")?; + } else { + write!(f, "SET TRANSACTION")?; + } + if !modes.is_empty() { + write!(f, " {}", display_comma_separated(modes))?; + } + if let Some(snapshot_id) = snapshot { + write!(f, " SNAPSHOT {snapshot_id}")?; + } + Ok(()) + } + Self::SetTimeZone { local, value } => { + f.write_str("SET ")?; + if *local { + f.write_str("LOCAL ")?; + } + write!(f, "TIME ZONE {value}") + } + Self::SetNames { + charset_name, + collation_name, + } => { + write!(f, "SET NAMES {charset_name}")?; + + if let Some(collation) = collation_name { + f.write_str(" COLLATE ")?; + f.write_str(collation)?; + }; + + Ok(()) + } + Self::SetNamesDefault {} => { + f.write_str("SET NAMES DEFAULT")?; + + Ok(()) + } + Set::SingleAssignment { + scope, + hivevar, + variable, + values, + } => { + write!( + f, + "SET {}{}{} = {}", + scope.map(|s| format!("{s}")).unwrap_or_default(), + if *hivevar { "HIVEVAR:" } else { "" }, + variable, + display_comma_separated(values) + ) + } + } + } +} + +/// 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(()) + } +} + +/// ANALYZE TABLE statement (Hive-specific) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Analyze { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + pub partitions: Option>, + pub for_columns: bool, + pub columns: Vec, + pub cache_metadata: bool, + pub noscan: bool, + pub compute_statistics: bool, + pub has_table_keyword: bool, +} + +impl fmt::Display for Analyze { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ANALYZE{}{table_name}", + if self.has_table_keyword { + " TABLE " + } else { + " " + }, + table_name = self.table_name + )?; + if let Some(ref parts) = self.partitions { + if !parts.is_empty() { + write!(f, " PARTITION ({})", display_comma_separated(parts))?; + } + } + + if self.compute_statistics { + write!(f, " COMPUTE STATISTICS")?; + } + if self.noscan { + write!(f, " NOSCAN")?; + } + if self.cache_metadata { + write!(f, " CACHE METADATA")?; + } + if self.for_columns { + write!(f, " FOR COLUMNS")?; + if !self.columns.is_empty() { + write!(f, " {}", display_comma_separated(&self.columns))?; + } + } + Ok(()) + } +} + +/// A top-level statement (SELECT, INSERT, CREATE, etc.) +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "visitor", + derive(Visit, VisitMut), + visit(with = "visit_statement") +)] +pub enum Statement { + /// ```sql + /// ANALYZE + /// ``` + /// Analyze (Hive) + Analyze(Analyze), + Set(Set), /// ```sql /// TRUNCATE /// ``` /// Truncate (Hive) - Truncate { - table_names: Vec, - partitions: Option>, - /// 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 - /// [ CASCADE | RESTRICT ] - cascade: Option, - /// ClickHouse-specific option - /// [ ON CLUSTER cluster_name ] - /// - /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/truncate/) - on_cluster: Option, - }, + Truncate(Truncate), /// ```sql /// MSCK /// ``` /// Msck (Hive) - Msck { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - repair: bool, - partition_action: Option, - }, + Msck(Msck), /// ```sql /// SELECT /// ``` @@ -2415,6 +3205,14 @@ pub enum Statement { file_format: Option, source: Box, }, + /// A `CASE` statement. + Case(CaseStatement), + /// An `IF` statement. + If(IfStatement), + /// A `WHILE` statement. + While(WhileStatement), + /// A `RAISE` statement. + Raise(RaiseStatement), /// ```sql /// CALL /// ``` @@ -2437,26 +3235,38 @@ pub enum Statement { values: Vec>, }, /// ```sql - /// COPY INTO + /// COPY INTO | /// ``` - /// See + /// See: + /// + /// + /// /// Copy Into syntax available for Snowflake is different than the one implemented in /// Postgres. Although they share common prefix, it is reasonable to implement them /// in different enums. This can be refactored later once custom dialects /// are allowed to have custom Statements. CopyIntoSnowflake { + kind: CopyIntoSnowflakeKind, into: ObjectName, - from_stage: ObjectName, - from_stage_alias: Option, + into_columns: Option>, + from_obj: Option, + from_obj_alias: Option, stage_params: StageParamsObject, - from_transformations: Option>, + from_transformations: Option>, + from_query: Option>, files: Option>, pattern: Option, - file_format: DataLoadingOptions, - copy_options: DataLoadingOptions, + file_format: KeyValueOptions, + copy_options: KeyValueOptions, validation_mode: Option, + partition: Option>, }, /// ```sql + /// OPEN cursor_name + /// ``` + /// Opens a cursor. + Open(OpenStatement), + /// ```sql /// CLOSE /// ``` /// Closes the portal underlying an open cursor. @@ -2467,20 +3277,7 @@ pub enum Statement { /// ```sql /// UPDATE /// ``` - Update { - /// TABLE - table: TableWithJoins, - /// Column assignments - assignments: Vec, - /// Table which provide value to be set - from: Option, - /// WHERE - selection: Option, - /// RETURNING - returning: Option>, - /// SQLite-specific conflict resolution clause - or: Option, - }, + Update(Update), /// ```sql /// DELETE /// ``` @@ -2488,30 +3285,7 @@ pub enum Statement { /// ```sql /// CREATE VIEW /// ``` - CreateView { - or_replace: bool, - materialized: bool, - /// View name - name: ObjectName, - columns: Vec, - query: Box, - options: CreateTableOptions, - cluster_by: Vec, - /// Snowflake: Views can have comments in Snowflake. - /// - comment: Option, - /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause - with_no_schema_binding: bool, - /// if true, has SQLite `IF NOT EXISTS` clause - if_not_exists: bool, - /// if true, has SQLite `TEMP` or `TEMPORARY` clause - temporary: bool, - /// if not None, has Clickhouse `TO` clause, specify the table into which to insert results - /// - to: Option, - /// MySQL: Optional parameters for the view algorithm, definer, and security context - params: Option, - }, + CreateView(CreateView), /// ```sql /// CREATE TABLE /// ``` @@ -2534,33 +3308,12 @@ pub enum Statement { /// ```sql /// CREATE ROLE /// ``` - /// See [postgres](https://www.postgresql.org/docs/current/sql-createrole.html) - CreateRole { - names: Vec, - if_not_exists: bool, - // Postgres - login: Option, - inherit: Option, - bypassrls: Option, - password: Option, - superuser: Option, - create_db: Option, - create_role: Option, - replication: Option, - connection_limit: Option, - valid_until: Option, - in_role: Vec, - in_group: Vec, - role: Vec, - user: Vec, - admin: Vec, - // MSSQL - authorization_owner: Option, - }, + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrole.html) + CreateRole(CreateRole), /// ```sql /// CREATE SECRET /// ``` - /// See [duckdb](https://duckdb.org/docs/sql/statements/create_secret.html) + /// See [DuckDB](https://duckdb.org/docs/sql/statements/create_secret.html) CreateSecret { or_replace: bool, temporary: Option, @@ -2570,6 +3323,8 @@ pub enum Statement { secret_type: Ident, options: Vec, }, + /// A `CREATE SERVER` statement. + CreateServer(CreateServerStatement), /// ```sql /// CREATE POLICY /// ``` @@ -2585,21 +3340,19 @@ pub enum Statement { with_check: Option, }, /// ```sql + /// CREATE CONNECTOR + /// ``` + /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-CreateDataConnectorCreateConnector) + CreateConnector(CreateConnector), + /// ```sql /// ALTER TABLE /// ``` - AlterTable { - /// Table name - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - name: ObjectName, - if_exists: bool, - only: bool, - operations: Vec, - location: Option, - /// ClickHouse dialect supports `ON CLUSTER` clause for ALTER TABLE - /// 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, - }, + AlterTable(AlterTable), + /// ```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 /// ``` @@ -2619,6 +3372,11 @@ pub enum Statement { with_options: Vec, }, /// ```sql + /// ALTER TYPE + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertype.html) + /// ``` + AlterType(AlterType), + /// ```sql /// ALTER ROLE /// ``` AlterRole { @@ -2636,6 +3394,31 @@ pub enum Statement { operation: AlterPolicyOperation, }, /// ```sql + /// ALTER CONNECTOR connector_name SET DCPROPERTIES(property_name=property_value, ...); + /// or + /// ALTER CONNECTOR connector_name SET URL new_url; + /// or + /// ALTER CONNECTOR connector_name SET OWNER [USER|ROLE] user_or_role; + /// ``` + /// (Hive-specific) + AlterConnector { + name: Ident, + properties: Option>, + url: Option, + owner: Option, + }, + /// ```sql + /// ALTER SESSION SET sessionParam + /// ALTER SESSION UNSET [ , , ... ] + /// ``` + /// See + AlterSession { + /// true is to set for the session parameters, false is to unset + set: bool, + /// The session parameters to set or unset + session_params: KeyValueOptions, + }, + /// ```sql /// ATTACH DATABASE 'path/to/file' AS alias /// ``` /// (SQLite-specific) @@ -2693,17 +3476,22 @@ 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 /// ``` - DropFunction { - if_exists: bool, - /// One or more function to drop - func_desc: Vec, - /// `CASCADE` or `RESTRICT` - drop_behavior: Option, - }, + DropFunction(DropFunction), + /// ```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 /// ``` @@ -2734,13 +3522,23 @@ pub enum Statement { drop_behavior: Option, }, /// ```sql + /// DROP CONNECTOR + /// ``` + /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-DropConnector) + DropConnector { + if_exists: bool, + name: Ident, + }, + /// ```sql /// DECLARE /// ``` /// Declare Cursor Variables /// /// Note: this is a PostgreSQL-specific statement, /// but may also compatible with other SQL. - Declare { stmts: Vec }, + Declare { + stmts: Vec, + }, /// ```sql /// CREATE EXTENSION [ IF NOT EXISTS ] extension_name /// [ WITH ] [ SCHEMA schema_name ] @@ -2749,25 +3547,13 @@ pub enum Statement { /// ``` /// /// Note: this is a PostgreSQL-specific statement, - CreateExtension { - name: Ident, - if_not_exists: bool, - cascade: bool, - schema: Option, - version: Option, - }, + CreateExtension(CreateExtension), /// ```sql /// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] - /// - /// Note: this is a PostgreSQL-specific statement. - /// https://www.postgresql.org/docs/current/sql-dropextension.html /// ``` - DropExtension { - names: Vec, - if_exists: bool, - /// `CASCADE` or `RESTRICT` - cascade_or_restrict: Option, - }, + /// Note: this is a PostgreSQL-specific statement. + /// + DropExtension(DropExtension), /// ```sql /// FETCH /// ``` @@ -2779,6 +3565,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, }, @@ -2802,69 +3589,23 @@ pub enum Statement { /// /// Note: this is a PostgreSQL-specific statement, /// but may also compatible with other SQL. - Discard { object_type: DiscardObject }, - /// ```sql - /// SET [ SESSION | LOCAL ] ROLE role_name - /// ``` - /// - /// Sets session state. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4] - /// - /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#set-role-statement - /// [2]: https://www.postgresql.org/docs/14/sql-set-role.html - /// [3]: https://dev.mysql.com/doc/refman/8.0/en/set-role.html - /// [4]: https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10004.htm - SetRole { - /// Non-ANSI optional identifier to inform if the role is defined inside the current session (`SESSION`) or transaction (`LOCAL`). - context_modifier: ContextModifier, - /// Role name. If NONE is specified, then the current role name is removed. - role_name: Option, - }, - /// ```sql - /// SET = expression; - /// SET (variable[, ...]) = (expression[, ...]); - /// ``` - /// - /// Note: this is not a standard SQL statement, but it is supported by at - /// least MySQL and PostgreSQL. Not all MySQL-specific syntactic forms are - /// supported yet. - SetVariable { - local: bool, - hivevar: bool, - variables: OneOrManyWithParens, - value: Vec, + Discard { + object_type: DiscardObject, }, - /// ```sql - /// SET TIME ZONE - /// ``` - /// - /// Note: this is a PostgreSQL-specific statements - /// `SET TIME ZONE ` is an alias for `SET timezone TO ` in PostgreSQL - SetTimeZone { local: bool, value: Expr }, - /// ```sql - /// SET NAMES 'charset_name' [COLLATE 'collation_name'] - /// ``` - /// - /// Note: this is a MySQL-specific statement. - SetNames { - charset_name: String, - collation_name: Option, - }, - /// ```sql - /// SET NAMES DEFAULT - /// ``` - /// - /// Note: this is a MySQL-specific statement. - SetNamesDefault {}, /// `SHOW FUNCTIONS` /// /// Note: this is a Presto-specific statement. - ShowFunctions { filter: Option }, + ShowFunctions { + filter: Option, + }, /// ```sql /// SHOW /// ``` /// /// Note: this is a PostgreSQL-specific statement. - ShowVariable { variable: Vec }, + ShowVariable { + variable: Vec, + }, /// ```sql /// SHOW [GLOBAL | SESSION] STATUS [LIKE 'pattern' | WHERE expr] /// ``` @@ -2918,6 +3659,18 @@ pub enum Statement { history: bool, show_options: ShowStatementOptions, }, + // ```sql + // SHOW {CHARACTER SET | CHARSET} + // ``` + // [MySQL]: + // + ShowCharset(ShowCharset), + /// ```sql + /// SHOW OBJECTS LIKE 'line%' IN mydb.public + /// ``` + /// Snowflake-specific statement + /// + ShowObjects(ShowObjects), /// ```sql /// SHOW TABLES /// ``` @@ -2942,7 +3695,9 @@ pub enum Statement { /// ``` /// /// Note: this is a MySQL-specific statement. - ShowCollation { filter: Option }, + ShowCollation { + filter: Option, + }, /// ```sql /// `USE ...` /// ``` @@ -2961,14 +3716,31 @@ pub enum Statement { begin: bool, transaction: Option, modifier: Option, - }, - /// ```sql - /// SET TRANSACTION ... - /// ``` - SetTransaction { - modes: Vec, - snapshot: Option, - session: bool, + /// List of statements belonging to the `BEGIN` block. + /// Example: + /// ```sql + /// BEGIN + /// SELECT 1; + /// SELECT 2; + /// END; + /// ``` + statements: Vec, + /// Exception handling with exception clauses. + /// Example: + /// ```sql + /// EXCEPTION + /// WHEN EXCEPTION_1 THEN + /// SELECT 2; + /// WHEN EXCEPTION_2 OR EXCEPTION_3 THEN + /// SELECT 3; + /// WHEN OTHER THEN + /// SELECT 4; + /// ``` + /// + /// + exception: Option>, + /// TRUE if the statement has an `END` keyword. + has_end_keyword: bool, }, /// ```sql /// COMMENT ON ... @@ -3011,15 +3783,65 @@ 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 + /// CREATE SCHEMA myschema OPTIONS(key1='value1'); + /// ``` + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_schema_statement) + options: Option>, + /// Default collation specification for the schema. + /// + /// ```sql + /// CREATE SCHEMA myschema DEFAULT COLLATE 'und:ci'; + /// ``` + /// + /// [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 @@ -3027,99 +3849,14 @@ pub enum Statement { /// /// Supported variants: /// 1. [Hive](https://cwiki.apache.org/confluence/display/hive/languagemanual+ddl#LanguageManualDDL-Create/Drop/ReloadFunction) - /// 2. [Postgres](https://www.postgresql.org/docs/15/sql-createfunction.html) + /// 2. [PostgreSQL](https://www.postgresql.org/docs/15/sql-createfunction.html) /// 3. [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement) + /// 4. [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql) CreateFunction(CreateFunction), - /// CREATE TRIGGER - /// - /// Examples: - /// - /// ```sql - /// CREATE TRIGGER trigger_name - /// BEFORE INSERT ON table_name - /// FOR EACH ROW - /// EXECUTE FUNCTION trigger_function(); - /// ``` - /// - /// Postgres: - CreateTrigger { - /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. - /// - /// Example: - /// ```sql - /// CREATE OR REPLACE TRIGGER trigger_name - /// AFTER INSERT ON table_name - /// FOR EACH ROW - /// EXECUTE FUNCTION trigger_function(); - /// ``` - or_replace: bool, - /// The `CONSTRAINT` keyword is used to create a trigger as a constraint. - is_constraint: bool, - /// The name of the trigger to be created. - name: ObjectName, - /// Determines whether the function is called before, after, or instead of the event. - /// - /// Example of BEFORE: - /// - /// ```sql - /// CREATE TRIGGER trigger_name - /// BEFORE INSERT ON table_name - /// FOR EACH ROW - /// EXECUTE FUNCTION trigger_function(); - /// ``` - /// - /// Example of AFTER: - /// - /// ```sql - /// CREATE TRIGGER trigger_name - /// AFTER INSERT ON table_name - /// FOR EACH ROW - /// EXECUTE FUNCTION trigger_function(); - /// ``` - /// - /// Example of INSTEAD OF: - /// - /// ```sql - /// CREATE TRIGGER trigger_name - /// INSTEAD OF INSERT ON table_name - /// FOR EACH ROW - /// EXECUTE FUNCTION trigger_function(); - /// ``` - period: TriggerPeriod, - /// 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. - table_name: ObjectName, - /// The optional referenced table name that can be referenced via - /// the `FROM` keyword. - referenced_table_name: Option, - /// This keyword immediately precedes the declaration of one or two relation names that provide access to the transition relations of the triggering statement. - referencing: Vec, - /// This specifies whether the trigger function should be fired once for - /// every row affected by the trigger event, or just once per SQL statement. - trigger_object: TriggerObject, - /// Whether to include the `EACH` term of the `FOR EACH`, as it is optional syntax. - include_each: bool, - /// Triggering conditions - condition: Option, - /// Execute logic block - exec_body: TriggerExecBody, - /// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`, - characteristics: Option, - }, - /// DROP TRIGGER - /// - /// ```sql - /// DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] - /// ``` - /// - DropTrigger { - if_exists: bool, - trigger_name: ObjectName, - table_name: ObjectName, - /// `CASCADE` or `RESTRICT` - option: Option, - }, + /// CREATE TRIGGER statement. See struct [CreateTrigger] for details. + CreateTrigger(CreateTrigger), + /// DROP TRIGGER statement. See struct [DropTrigger] for details. + DropTrigger(DropTrigger), /// ```sql /// CREATE PROCEDURE /// ``` @@ -3127,7 +3864,8 @@ pub enum Statement { or_alter: bool, name: ObjectName, params: Option>, - body: Vec, + language: Option, + body: ConditionalStatements, }, /// ```sql /// CREATE MACRO @@ -3152,9 +3890,9 @@ pub enum Statement { if_not_exists: bool, name: ObjectName, stage_params: StageParamsObject, - directory_table_params: DataLoadingOptions, - file_format: DataLoadingOptions, - copy_options: DataLoadingOptions, + directory_table_params: KeyValueOptions, + file_format: KeyValueOptions, + copy_options: KeyValueOptions, comment: Option, }, /// ```sql @@ -3169,17 +3907,23 @@ pub enum Statement { /// ``` Grant { privileges: Privileges, - objects: GrantObjects, + 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 { privileges: Privileges, - objects: GrantObjects, + objects: Option, grantees: Vec, granted_by: Option, cascade: Option, @@ -3189,20 +3933,32 @@ pub enum Statement { /// ``` /// /// Note: this is a PostgreSQL-specific statement. - Deallocate { name: Ident, prepare: bool }, + Deallocate { + name: Ident, + prepare: bool, + }, /// ```sql - /// EXECUTE name [ ( parameter [, ...] ) ] [USING ] + /// An `EXECUTE` statement /// ``` /// - /// Note: this statement is supported by Postgres and MSSQL, with slight differences in syntax. - /// /// Postgres: /// MSSQL: + /// BigQuery: + /// Snowflake: Execute { - name: ObjectName, + name: Option, parameters: Vec, has_parentheses: bool, - using: Vec, + /// Is this an `EXECUTE IMMEDIATE` + 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 @@ -3264,7 +4020,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>, }, @@ -3272,11 +4028,15 @@ pub enum Statement { /// SAVEPOINT /// ``` /// Define a new savepoint within the current transaction - Savepoint { name: Ident }, + Savepoint { + name: Ident, + }, /// ```sql /// RELEASE [ SAVEPOINT ] savepoint_name /// ``` - ReleaseSavepoint { name: Ident }, + ReleaseSavepoint { + name: Ident, + }, /// A `MERGE` statement. /// /// ```sql @@ -3284,6 +4044,7 @@ pub enum Statement { /// ``` /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + /// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16) Merge { /// optional INTO keyword into: bool, @@ -3295,6 +4056,8 @@ pub enum Statement { on: Box, /// Specifies the actions to perform when values match or do not match. clauses: Vec, + // Specifies the output to save changes in MSSQL + output: Option, }, /// ```sql /// CACHE [ FLAG ] TABLE [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ ] @@ -3337,6 +4100,8 @@ pub enum Statement { sequence_options: Vec, owned_by: Option, }, + /// A `CREATE DOMAIN` statement. + CreateDomain(CreateDomain), /// ```sql /// CREATE TYPE /// ``` @@ -3356,21 +4121,32 @@ pub enum Statement { /// LOCK TABLES [READ [LOCAL] | [LOW_PRIORITY] WRITE] /// ``` /// Note: this is a MySQL-specific statement. See - LockTables { tables: Vec }, + LockTables { + tables: Vec, + }, /// ```sql /// UNLOCK TABLES /// ``` /// Note: this is a MySQL-specific statement. See UnlockTables, + /// Unloads the result of a query to file + /// + /// [Athena](https://docs.aws.amazon.com/athena/latest/ug/unload.html): /// ```sql /// UNLOAD(statement) TO [ WITH options ] /// ``` - /// See Redshift and - // Athena + /// + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_UNLOAD.html): + /// ```sql + /// UNLOAD('statement') TO [ OPTIONS ] + /// ``` Unload { - query: Box, + query: Option>, + query_text: Option, to: Ident, + auth: Option, with: Vec, + options: Vec, }, /// ```sql /// OPTIMIZE TABLE [db.]name [ON CLUSTER cluster] [PARTITION partition | PARTITION ID 'partition_id'] [FINAL] [DEDUPLICATE [BY expression]] @@ -3390,18 +4166,22 @@ pub enum Statement { /// listen for a notification channel /// /// See Postgres - LISTEN { channel: Ident }, + LISTEN { + channel: Ident, + }, /// ```sql /// UNLISTEN /// ``` /// stop listening for a notification /// /// See Postgres - UNLISTEN { channel: Ident }, + UNLISTEN { + channel: Ident, + }, /// ```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 { @@ -3437,13 +4217,149 @@ pub enum Statement { /// Snowflake `REMOVE` /// See: Remove(FileStagingCommand), - /// MS-SQL session + /// RaiseError (MSSQL) + /// RAISERROR ( { msg_id | msg_str | @local_variable } + /// { , severity , state } + /// [ , argument [ , ...n ] ] ) + /// [ WITH option [ , ...n ] ] + /// See + RaisError { + message: Box, + severity: Box, + state: Box, + arguments: Vec, + options: Vec, + }, + /// ```sql + /// PRINT msg_str | @local_variable | string_expr + /// ``` /// - /// See - SetSessionParam(SetSessionParamKind), + /// See: + Print(PrintStatement), + /// ```sql + /// RETURN [ expression ] + /// ``` + /// + /// 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), + /// ```sql + /// ALTER USER \[ IF EXISTS \] \[ \] + /// ``` + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/alter-user) + AlterUser(AlterUser), + /// 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), + /// Restore the value of a run-time parameter to the default value. + /// + /// ```sql + /// RESET configuration_parameter; + /// RESET ALL; + /// ``` + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-reset.html) + Reset(ResetStatement), +} + +impl From for Statement { + fn from(analyze: Analyze) -> Self { + Statement::Analyze(analyze) + } +} + +impl From for Statement { + fn from(truncate: ddl::Truncate) -> Self { + Statement::Truncate(truncate) + } +} + +impl From for Statement { + fn from(msck: ddl::Msck) -> Self { + Statement::Msck(msck) + } +} + +/// ```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)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RaisErrorOption { + Log, + NoWait, + SetError, +} + +impl fmt::Display for RaisErrorOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RaisErrorOption::Log => write!(f, "LOG"), + RaisErrorOption::NoWait => write!(f, "NOWAIT"), + RaisErrorOption::SetError => write!(f, "SETERROR"), + } + } } 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)] @@ -3459,7 +4375,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}")?; @@ -3497,7 +4414,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 ")?; @@ -3532,7 +4449,7 @@ impl fmt::Display for Statement { } if let Some(format) = format { - write!(f, "FORMAT {format} ")?; + write!(f, "{format} ")?; } if let Some(options) = options { @@ -3541,7 +4458,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, "; ")) @@ -3549,11 +4466,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}")?; @@ -3580,62 +4496,19 @@ impl fmt::Display for Statement { } write!(f, " {source}") } - Statement::Msck { - table_name, - repair, - partition_action, - } => { - write!( - f, - "MSCK {repair}TABLE {table}", - repair = if *repair { "REPAIR " } else { "" }, - table = table_name - )?; - if let Some(pa) = partition_action { - write!(f, " {pa}")?; - } - Ok(()) + Statement::Msck(msck) => msck.fmt(f), + Statement::Truncate(truncate) => truncate.fmt(f), + Statement::Case(stmt) => { + write!(f, "{stmt}") } - Statement::Truncate { - 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}", - table_names = display_comma_separated(table_names) - )?; - - if let Some(identity) = identity { - match identity { - TruncateIdentityOption::Restart => write!(f, " RESTART IDENTITY")?, - TruncateIdentityOption::Continue => write!(f, " CONTINUE IDENTITY")?, - } - } - if let Some(cascade) = cascade { - match cascade { - CascadeOption::Cascade => write!(f, " CASCADE")?, - CascadeOption::Restrict => write!(f, " RESTRICT")?, - } - } - - if let Some(ref parts) = partitions { - if !parts.is_empty() { - write!(f, " PARTITION ({})", display_comma_separated(parts))?; - } - } - if let Some(on_cluster) = on_cluster { - write!(f, " ON CLUSTER {on_cluster}")?; - } - Ok(()) + Statement::If(stmt) => { + write!(f, "{stmt}") + } + Statement::While(stmt) => { + write!(f, "{stmt}") + } + Statement::Raise(stmt) => { + write!(f, "{stmt}") } Statement::AttachDatabase { schema_name, @@ -3679,45 +4552,8 @@ impl fmt::Display for Statement { )?; Ok(()) } - Statement::Analyze { - table_name, - partitions, - for_columns, - columns, - cache_metadata, - noscan, - compute_statistics, - has_table_keyword, - } => { - write!( - f, - "ANALYZE{}{table_name}", - if *has_table_keyword { " TABLE " } else { " " } - )?; - if let Some(ref parts) = partitions { - if !parts.is_empty() { - write!(f, " PARTITION ({})", display_comma_separated(parts))?; - } - } - - if *compute_statistics { - write!(f, " COMPUTE STATISTICS")?; - } - if *noscan { - write!(f, " NOSCAN")?; - } - if *cache_metadata { - write!(f, " CACHE METADATA")?; - } - if *for_columns { - write!(f, " FOR COLUMNS")?; - if !columns.is_empty() { - write!(f, " {}", display_comma_separated(columns))?; - } - } - Ok(()) - } - Statement::Insert(insert) => write!(f, "{insert}"), + Statement::Analyze(analyze) => analyze.fmt(f), + Statement::Insert(insert) => insert.fmt(f), Statement::Install { extension_name: name, } => write!(f, "INSTALL {name}"), @@ -3772,37 +4608,9 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Update { - table, - assignments, - from, - selection, - returning, - or, - } => { - write!(f, "UPDATE ")?; - if let Some(or) = or { - write!(f, "{or} ")?; - } - write!(f, "{table}")?; - if let Some(UpdateTableFromKind::BeforeSet(from)) = from { - write!(f, " FROM {from}")?; - } - if !assignments.is_empty() { - write!(f, " SET {}", display_comma_separated(assignments))?; - } - if let Some(UpdateTableFromKind::AfterSet(from)) = from { - write!(f, " FROM {from}")?; - } - if let Some(selection) = selection { - write!(f, " WHERE {selection}")?; - } - if let Some(returning) = returning { - write!(f, " RETURNING {}", display_comma_separated(returning))?; - } - Ok(()) - } - Statement::Delete(delete) => write!(f, "{delete}"), + Statement::Update(update) => update.fmt(f), + Statement::Delete(delete) => delete.fmt(f), + Statement::Open(open) => open.fmt(f), Statement::Close { cursor } => { write!(f, "CLOSE {cursor}")?; @@ -3813,90 +4621,106 @@ 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}")?; - if let Some(l) = location { + 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}'")?; } - Ok(()) - } - Statement::CreateFunction(create_function) => create_function.fmt(f), - Statement::CreateTrigger { - or_replace, - is_constraint, - name, - period, - events, - table_name, - referenced_table_name, - referencing, - trigger_object, - condition, - include_each, - exec_body, - characteristics, - } => { - write!( - f, - "CREATE {or_replace}{is_constraint}TRIGGER {name} {period}", - or_replace = if *or_replace { "OR REPLACE " } else { "" }, - is_constraint = if *is_constraint { "CONSTRAINT " } else { "" }, - )?; + if let Some(clone) = clone { + write!(f, " CLONE {clone}")?; + } - if !events.is_empty() { - write!(f, " {}", display_separated(events, " OR "))?; + if let Some(value) = data_retention_time_in_days { + write!(f, " DATA_RETENTION_TIME_IN_DAYS = {value}")?; } - write!(f, " ON {table_name}")?; - if let Some(referenced_table_name) = referenced_table_name { - write!(f, " FROM {referenced_table_name}")?; + if let Some(value) = max_data_extension_time_in_days { + write!(f, " MAX_DATA_EXTENSION_TIME_IN_DAYS = {value}")?; } - if let Some(characteristics) = characteristics { - write!(f, " {characteristics}")?; + if let Some(vol) = external_volume { + write!(f, " EXTERNAL_VOLUME = '{vol}'")?; } - if !referencing.is_empty() { - write!(f, " REFERENCING {}", display_separated(referencing, " "))?; + if let Some(cat) = catalog { + write!(f, " CATALOG = '{cat}'")?; } - if *include_each { - write!(f, " FOR EACH {trigger_object}")?; - } else { - write!(f, " FOR {trigger_object}")?; + 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(condition) = condition { - write!(f, " WHEN {condition}")?; + + if let Some(collation) = default_ddl_collation { + write!(f, " DEFAULT_DDL_COLLATION = '{collation}'")?; } - write!(f, " EXECUTE {exec_body}") - } - Statement::DropTrigger { - if_exists, - trigger_name, - table_name, - option, - } => { - write!(f, "DROP TRIGGER")?; - if *if_exists { - write!(f, " IF EXISTS")?; + + 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))?; } - write!(f, " {trigger_name} ON {table_name}")?; - if let Some(option) = option { - write!(f, " {option}")?; + + 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(create_trigger) => create_trigger.fmt(f), + Statement::DropTrigger(drop_trigger) => drop_trigger.fmt(f), Statement::CreateProcedure { name, or_alter, params, + language, body, } => { write!( @@ -3911,11 +4735,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, @@ -3939,66 +4764,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::CreateView { - name, - or_replace, - columns, - query, - materialized, - options, - cluster_by, - comment, - with_no_schema_binding, - if_not_exists, - temporary, - to, - params, - } => { - write!( - f, - "CREATE {or_replace}", - or_replace = if *or_replace { "OR REPLACE " } else { "" }, - )?; - if let Some(params) = params { - params.fmt(f)?; - } - write!( - f, - "{materialized}{temporary}VIEW {if_not_exists}{name}{to}", - 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}")) - .unwrap_or_default() - )?; - if !columns.is_empty() { - write!(f, " ({})", display_comma_separated(columns))?; - } - if matches!(options, CreateTableOptions::With(_)) { - write!(f, " {options}")?; - } - if let Some(comment) = comment { - write!( - f, - " COMMENT = '{}'", - value::escape_single_quote_string(comment) - )?; - } - if !cluster_by.is_empty() { - write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; - } - if matches!(options, CreateTableOptions::Options(_)) { - write!(f, " {options}")?; - } - write!(f, " AS {query}")?; - if *with_no_schema_binding { - write!(f, " WITH NO SCHEMA BINDING")?; - } - Ok(()) - } + Statement::CreateView(create_view) => create_view.fmt(f), Statement::CreateTable(create_table) => create_table.fmt(f), Statement::LoadData { local, @@ -4049,141 +4815,9 @@ impl fmt::Display for Statement { Ok(()) } Statement::CreateIndex(create_index) => create_index.fmt(f), - Statement::CreateExtension { - name, - if_not_exists, - cascade, - schema, - version, - } => { - write!( - f, - "CREATE EXTENSION {if_not_exists}{name}", - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" } - )?; - if *cascade || schema.is_some() || version.is_some() { - write!(f, " WITH")?; - - if let Some(name) = schema { - write!(f, " SCHEMA {name}")?; - } - if let Some(version) = version { - write!(f, " VERSION {version}")?; - } - if *cascade { - write!(f, " CASCADE")?; - } - } - - Ok(()) - } - Statement::DropExtension { - names, - if_exists, - cascade_or_restrict, - } => { - write!(f, "DROP EXTENSION")?; - if *if_exists { - write!(f, " IF EXISTS")?; - } - write!(f, " {}", display_comma_separated(names))?; - if let Some(cascade_or_restrict) = cascade_or_restrict { - write!(f, " {cascade_or_restrict}")?; - } - Ok(()) - } - Statement::CreateRole { - names, - if_not_exists, - inherit, - login, - bypassrls, - password, - create_db, - create_role, - superuser, - replication, - connection_limit, - valid_until, - in_role, - in_group, - role, - user, - admin, - authorization_owner, - } => { - write!( - f, - "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - names = display_separated(names, ", "), - superuser = match *superuser { - Some(true) => " SUPERUSER", - Some(false) => " NOSUPERUSER", - None => "" - }, - create_db = match *create_db { - Some(true) => " CREATEDB", - Some(false) => " NOCREATEDB", - None => "" - }, - create_role = match *create_role { - Some(true) => " CREATEROLE", - Some(false) => " NOCREATEROLE", - None => "" - }, - inherit = match *inherit { - Some(true) => " INHERIT", - Some(false) => " NOINHERIT", - None => "" - }, - login = match *login { - Some(true) => " LOGIN", - Some(false) => " NOLOGIN", - None => "" - }, - replication = match *replication { - Some(true) => " REPLICATION", - Some(false) => " NOREPLICATION", - None => "" - }, - bypassrls = match *bypassrls { - Some(true) => " BYPASSRLS", - Some(false) => " NOBYPASSRLS", - None => "" - } - )?; - if let Some(limit) = connection_limit { - write!(f, " CONNECTION LIMIT {limit}")?; - } - match password { - Some(Password::Password(pass)) => write!(f, " PASSWORD {pass}"), - Some(Password::NullPassword) => write!(f, " PASSWORD NULL"), - None => Ok(()), - }?; - if let Some(until) = valid_until { - write!(f, " VALID UNTIL {until}")?; - } - if !in_role.is_empty() { - write!(f, " IN ROLE {}", display_comma_separated(in_role))?; - } - if !in_group.is_empty() { - write!(f, " IN GROUP {}", display_comma_separated(in_group))?; - } - if !role.is_empty() { - write!(f, " ROLE {}", display_comma_separated(role))?; - } - if !user.is_empty() { - write!(f, " USER {}", display_comma_separated(user))?; - } - if !admin.is_empty() { - write!(f, " ADMIN {}", display_comma_separated(admin))?; - } - if let Some(owner) = authorization_owner { - write!(f, " AUTHORIZATION {owner}")?; - } - Ok(()) - } + Statement::CreateExtension(create_extension) => write!(f, "{create_extension}"), + Statement::DropExtension(drop_extension) => write!(f, "{drop_extension}"), + Statement::CreateRole(create_role) => write!(f, "{create_role}"), Statement::CreateSecret { or_replace, temporary, @@ -4219,6 +4853,9 @@ impl fmt::Display for Statement { write!(f, " )")?; Ok(()) } + Statement::CreateServer(stmt) => { + write!(f, "{stmt}") + } Statement::CreatePolicy { name, table_name, @@ -4261,35 +4898,8 @@ impl fmt::Display for Statement { Ok(()) } - Statement::AlterTable { - name, - if_exists, - only, - operations, - location, - on_cluster, - } => { - write!(f, "ALTER TABLE ")?; - if *if_exists { - write!(f, "IF EXISTS ")?; - } - if *only { - write!(f, "ONLY ")?; - } - write!(f, "{name} ", name = name)?; - if let Some(cluster) = on_cluster { - write!(f, "ON CLUSTER {cluster} ")?; - } - write!( - f, - "{operations}", - operations = display_comma_separated(operations) - )?; - if let Some(loc) = location { - write!(f, " {loc}")? - } - Ok(()) - } + Statement::CreateConnector(create_connector) => create_connector.fmt(f), + Statement::AlterTable(alter_table) => write!(f, "{alter_table}"), Statement::AlterIndex { name, operation } => { write!(f, "ALTER INDEX {name} {operation}") } @@ -4308,6 +4918,9 @@ impl fmt::Display for Statement { } write!(f, " AS {query}") } + Statement::AlterType(AlterType { name, operation }) => { + write!(f, "ALTER TYPE {name} {operation}") + } Statement::AlterRole { name, operation } => { write!(f, "ALTER ROLE {name} {operation}") } @@ -4318,6 +4931,51 @@ impl fmt::Display for Statement { } => { write!(f, "ALTER POLICY {name} ON {table_name}{operation}") } + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + write!(f, "ALTER CONNECTOR {name}")?; + if let Some(properties) = properties { + write!( + f, + " SET DCPROPERTIES({})", + display_comma_separated(properties) + )?; + } + if let Some(url) = url { + write!(f, " SET URL '{url}'")?; + } + if let Some(owner) = owner { + write!(f, " SET OWNER {owner}")?; + } + Ok(()) + } + Statement::AlterSession { + set, + session_params, + } => { + write!( + f, + "ALTER SESSION {set}", + set = if *set { "SET" } else { "UNSET" } + )?; + if !session_params.options.is_empty() { + if *set { + write!(f, " {session_params}")?; + } else { + let options = session_params + .options + .iter() + .map(|p| p.option_name.clone()) + .collect::>(); + write!(f, " {}", display_separated(&options, ", "))?; + } + } + Ok(()) + } Statement::Drop { object_type, if_exists, @@ -4326,27 +4984,34 @@ 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 { "" } - ), - Statement::DropFunction { + 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(drop_function) => write!(f, "{drop_function}"), + Statement::DropDomain(DropDomain { if_exists, - func_desc, + name, drop_behavior, - } => { + }) => { write!( f, - "DROP FUNCTION{} {}", + "DROP DOMAIN{} {name}", if *if_exists { " IF EXISTS" } else { "" }, - display_comma_separated(func_desc), )?; if let Some(op) = drop_behavior { write!(f, " {op}")?; @@ -4405,64 +5070,19 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Discard { object_type } => { - write!(f, "DISCARD {object_type}")?; - Ok(()) - } - Self::SetRole { - context_modifier, - role_name, - } => { - let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE")); - write!(f, "SET{context_modifier} ROLE {role_name}") - } - Statement::SetVariable { - local, - variables, - hivevar, - value, - } => { - f.write_str("SET ")?; - if *local { - f.write_str("LOCAL ")?; - } - let parenthesized = matches!(variables, OneOrManyWithParens::Many(_)); + Statement::DropConnector { if_exists, name } => { write!( f, - "{hivevar}{name} = {l_paren}{value}{r_paren}", - hivevar = if *hivevar { "HIVEVAR:" } else { "" }, - name = variables, - l_paren = parenthesized.then_some("(").unwrap_or_default(), - value = display_comma_separated(value), - r_paren = parenthesized.then_some(")").unwrap_or_default(), - ) - } - Statement::SetTimeZone { local, value } => { - f.write_str("SET ")?; - if *local { - f.write_str("LOCAL ")?; - } - write!(f, "TIME ZONE {value}") - } - Statement::SetNames { - charset_name, - collation_name, - } => { - f.write_str("SET NAMES ")?; - f.write_str(charset_name)?; - - if let Some(collation) = collation_name { - f.write_str(" COLLATE ")?; - f.write_str(collation)?; - }; - + "DROP CONNECTOR {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" } + )?; Ok(()) } - Statement::SetNamesDefault {} => { - f.write_str("SET NAMES DEFAULT")?; - + Statement::Discard { object_type } => { + write!(f, "DISCARD {object_type}")?; Ok(()) } + Self::Set(set) => write!(f, "{set}"), Statement::ShowVariable { variable } => { write!(f, "SHOW")?; if !variable.is_empty() { @@ -4549,6 +5169,17 @@ impl fmt::Display for Statement { )?; Ok(()) } + Statement::ShowObjects(ShowObjects { + terse, + show_options, + }) => { + write!( + f, + "SHOW {terse}OBJECTS{show_options}", + terse = if *terse { "TERSE " } else { "" }, + )?; + Ok(()) + } Statement::ShowTables { terse, history, @@ -4596,15 +5227,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, + has_end_keyword, } => { if *syntax_begin { if let Some(modifier) = *modifier { - write!(f, "BEGIN {}", modifier)?; + write!(f, "BEGIN {modifier}")?; } else { write!(f, "BEGIN")?; } @@ -4617,23 +5252,18 @@ impl fmt::Display for Statement { if !modes.is_empty() { write!(f, " {}", display_comma_separated(modes))?; } - Ok(()) - } - Statement::SetTransaction { - modes, - snapshot, - session, - } => { - if *session { - write!(f, "SET SESSION CHARACTERISTICS AS TRANSACTION")?; - } else { - write!(f, "SET TRANSACTION")?; + if !statements.is_empty() { + write!(f, " ")?; + format_statement_list(f, statements)?; } - if !modes.is_empty() { - write!(f, " {}", display_comma_separated(modes))?; + if let Some(exception_when) = exception { + write!(f, " EXCEPTION")?; + for when in exception_when { + write!(f, " {when}")?; + } } - if let Some(snapshot_id) = snapshot { - write!(f, " SNAPSHOT {snapshot_id}")?; + if *has_end_keyword { + write!(f, " END")?; } Ok(()) } @@ -4645,7 +5275,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")?; @@ -4671,16 +5301,39 @@ impl fmt::Display for Statement { Statement::CreateSchema { schema_name, if_not_exists, - } => write!( - f, - "CREATE SCHEMA {if_not_exists}{name}", - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - name = schema_name - ), - Statement::Assert { condition, message } => { - write!(f, "ASSERT {condition}")?; - if let Some(m) = message { - write!(f, " AS {m}")?; + with, + options, + default_collate_spec, + clone, + } => { + write!( + f, + "CREATE SCHEMA {if_not_exists}{name}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + name = schema_name + )?; + + if let Some(collate) = default_collate_spec { + 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 } => { + write!(f, "ASSERT {condition}")?; + if let Some(m) = message { + write!(f, " AS {m}")?; } Ok(()) } @@ -4689,19 +5342,30 @@ impl fmt::Display for Statement { objects, grantees, with_grant_option, + as_grantor, granted_by, + current_grants, } => { write!(f, "GRANT {privileges} ")?; - write!(f, "ON {objects} ")?; + if let Some(objects) = objects { + write!(f, "ON {objects} ")?; + } write!(f, "TO {}", display_comma_separated(grantees))?; 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, @@ -4710,13 +5374,15 @@ impl fmt::Display for Statement { cascade, } => { write!(f, "REVOKE {privileges} ")?; - write!(f, "ON {objects} ")?; + if let Some(objects) = objects { + write!(f, "ON {objects} ")?; + } write!(f, "FROM {}", display_comma_separated(grantees))?; if let Some(grantor) = granted_by { write!(f, " GRANTED BY {grantor}")?; } if let Some(cascade) = cascade { - write!(f, " {}", cascade)?; + write!(f, " {cascade}")?; } Ok(()) } @@ -4730,21 +5396,37 @@ impl fmt::Display for Statement { name, parameters, has_parentheses, + immediate, + into, using, + output, + default, } => { let (open, close) = if *has_parentheses { ("(", ")") } else { (if parameters.is_empty() { "" } else { " " }, "") }; - write!( - f, - "EXECUTE {name}{open}{}{close}", - display_comma_separated(parameters), - )?; + write!(f, "EXECUTE")?; + if *immediate { + write!(f, " IMMEDIATE")?; + } + if let Some(name) = name { + write!(f, " {name}")?; + } + write!(f, "{open}{}{close}", display_comma_separated(parameters),)?; + if !into.is_empty() { + write!(f, " INTO {}", display_comma_separated(into))?; + } if !using.is_empty() { write!(f, " USING {}", display_comma_separated(using))?; }; + if *output { + write!(f, " OUTPUT")?; + } + if *default { + write!(f, " DEFAULT")?; + } Ok(()) } Statement::Prepare { @@ -4788,6 +5470,7 @@ impl fmt::Display for Statement { source, on, clauses, + output, } => { write!( f, @@ -4795,7 +5478,11 @@ impl fmt::Display for Statement { int = if *into { " INTO" } else { "" } )?; write!(f, "ON {on} ")?; - write!(f, "{}", display_separated(clauses, " ")) + write!(f, "{}", display_separated(clauses, " "))?; + if let Some(output) = output { + write!(f, " {output}")?; + } + Ok(()) } Statement::Cache { table_name, @@ -4882,13 +5569,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())?; @@ -4896,60 +5583,73 @@ impl fmt::Display for Statement { Ok(()) } Statement::CopyIntoSnowflake { + kind, into, - from_stage, - from_stage_alias, + into_columns, + from_obj, + from_obj_alias, stage_params, from_transformations, + from_query, files, pattern, file_format, copy_options, validation_mode, + partition, } => { - write!(f, "COPY INTO {}", into)?; - if from_transformations.is_none() { - // Standard data load - write!(f, " FROM {}{}", from_stage, stage_params)?; - if from_stage_alias.as_ref().is_some() { - write!(f, " AS {}", from_stage_alias.as_ref().unwrap())?; - } - } else { + write!(f, "COPY INTO {into}")?; + if let Some(into_columns) = into_columns { + write!(f, " ({})", display_comma_separated(into_columns))?; + } + if let Some(from_transformations) = from_transformations { // Data load with transformation - write!( - f, - " FROM (SELECT {} FROM {}{}", - display_separated(from_transformations.as_ref().unwrap(), ", "), - from_stage, - stage_params, - )?; - if from_stage_alias.as_ref().is_some() { - write!(f, " AS {}", from_stage_alias.as_ref().unwrap())?; + if let Some(from_stage) = from_obj { + write!( + f, + " FROM (SELECT {} FROM {}{}", + display_separated(from_transformations, ", "), + from_stage, + stage_params + )?; + } + if let Some(from_obj_alias) = 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}")?; + if let Some(from_obj_alias) = from_obj_alias { + write!(f, " AS {from_obj_alias}")?; + } + } else if let Some(from_query) = from_query { + // Data unload from query + write!(f, " FROM ({from_query})")?; } - if files.is_some() { - write!( - f, - " FILES = ('{}')", - display_separated(files.as_ref().unwrap(), "', '") - )?; + + if let Some(files) = files { + write!(f, " FILES = ('{}')", display_separated(files, "', '"))?; } - if pattern.is_some() { - write!(f, " PATTERN = '{}'", pattern.as_ref().unwrap())?; + if let Some(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() { - write!(f, " COPY_OPTIONS=({})", copy_options)?; + match kind { + CopyIntoSnowflakeKind::Table => { + write!(f, " COPY_OPTIONS=({copy_options})")? + } + CopyIntoSnowflakeKind::Location => write!(f, " {copy_options}")?, + } } - if validation_mode.is_some() { - write!( - f, - " VALIDATION_MODE = {}", - validation_mode.as_ref().unwrap() - )?; + if let Some(validation_mode) = validation_mode { + write!(f, " VALIDATION_MODE = {validation_mode}")?; } Ok(()) } @@ -4977,13 +5677,31 @@ impl fmt::Display for Statement { Statement::UnlockTables => { write!(f, "UNLOCK TABLES") } - Statement::Unload { query, to, with } => { - write!(f, "UNLOAD({query}) TO {to}")?; - + Statement::Unload { + query, + query_text, + to, + auth, + with, + options, + } => { + write!(f, "UNLOAD(")?; + if let Some(query) = query { + write!(f, "{query}")?; + } + if let Some(query_text) = query_text { + write!(f, "'{query_text}'")?; + } + write!(f, ") TO {to}")?; + if let Some(auth) = auth { + write!(f, " IAM_ROLE {auth}")?; + } if !with.is_empty() { write!(f, " WITH ({})", display_comma_separated(with))?; } - + if !options.is_empty() { + write!(f, " {}", display_separated(options, " "))?; + } Ok(()) } Statement::OptimizeTable { @@ -4995,10 +5713,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")?; @@ -5026,9 +5744,33 @@ impl fmt::Display for Statement { Statement::RenameTable(rename_tables) => { write!(f, "RENAME TABLE {}", display_comma_separated(rename_tables)) } + Statement::RaisError { + message, + severity, + state, + arguments, + options, + } => { + write!(f, "RAISERROR({message}, {severity}, {state}")?; + if !arguments.is_empty() { + write!(f, ", {}", display_comma_separated(arguments))?; + } + write!(f, ")")?; + if !options.is_empty() { + write!(f, " WITH {}", display_comma_separated(options))?; + } + Ok(()) + } + Statement::Print(s) => write!(f, "{s}"), + Statement::Return(r) => write!(f, "{r}"), Statement::List(command) => write!(f, "LIST {command}"), Statement::Remove(command) => write!(f, "REMOVE {command}"), - Statement::SetSessionParam(kind) => write!(f, "SET {kind}"), + Statement::ExportData(e) => write!(f, "{e}"), + Statement::CreateUser(s) => write!(f, "{s}"), + Statement::AlterSchema(s) => write!(f, "{s}"), + Statement::Vacuum(s) => write!(f, "{s}"), + Statement::AlterUser(s) => write!(f, "{s}"), + Statement::Reset(s) => write!(f, "{s}"), } } } @@ -5092,6 +5834,28 @@ impl fmt::Display for SequenceOptions { } } +/// Assignment for a `SET` statement (name [=|TO] value) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetAssignment { + pub scope: Option, + pub name: ObjectName, + pub value: Expr, +} + +impl fmt::Display for SetAssignment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}{} = {}", + self.scope.map(|s| format!("{s}")).unwrap_or_default(), + self.name, + self.value + ) + } +} + /// Target of a `TRUNCATE TABLE` command /// /// Note this is its own struct because `visit_relation` requires an `ObjectName` (not a `Vec`) @@ -5102,10 +5866,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) } } @@ -5374,34 +6145,163 @@ 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))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum Action { + AddSearchOptimization, + Apply { + apply_type: ActionApplyType, + }, + ApplyBudget, + AttachListing, + AttachPolicy, + Audit, + BindServiceEndpoint, Connect, - Create, + Create { + obj_type: Option, + }, + DatabaseRole { + role: ObjectName, + }, Delete, - Execute, - Insert { columns: Option> }, - References { columns: Option> }, - Select { columns: Option> }, + Drop, + EvolveSchema, + Exec { + obj_type: Option, + }, + Execute { + obj_type: Option, + }, + Failover, + ImportedPrivileges, + ImportShare, + Insert { + columns: Option>, + }, + Manage { + manage_type: ActionManageType, + }, + ManageReleases, + ManageVersions, + Modify { + modify_type: Option, + }, + Monitor { + monitor_type: Option, + }, + Operate, + OverrideShareRestrictions, + Ownership, + PurchaseDataExchangeListing, + Read, + ReadSession, + References { + columns: Option>, + }, + Replicate, + ResolveAll, + Role { + role: ObjectName, + }, + Select { + columns: Option>, + }, Temporary, Trigger, Truncate, - Update { columns: Option> }, + Update { + columns: Option>, + }, Usage, } impl fmt::Display for Action { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + Action::AddSearchOptimization => f.write_str("ADD SEARCH OPTIMIZATION")?, + Action::Apply { apply_type } => write!(f, "APPLY {apply_type}")?, + Action::ApplyBudget => f.write_str("APPLYBUDGET")?, + Action::AttachListing => f.write_str("ATTACH LISTING")?, + Action::AttachPolicy => f.write_str("ATTACH POLICY")?, + Action::Audit => f.write_str("AUDIT")?, + Action::BindServiceEndpoint => f.write_str("BIND SERVICE ENDPOINT")?, Action::Connect => f.write_str("CONNECT")?, - Action::Create => f.write_str("CREATE")?, + Action::Create { obj_type } => { + f.write_str("CREATE")?; + if let Some(obj_type) = obj_type { + write!(f, " {obj_type}")? + } + } + Action::DatabaseRole { role } => write!(f, "DATABASE ROLE {role}")?, Action::Delete => f.write_str("DELETE")?, - Action::Execute => f.write_str("EXECUTE")?, + 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 { + write!(f, " {obj_type}")? + } + } + Action::Failover => f.write_str("FAILOVER")?, + Action::ImportedPrivileges => f.write_str("IMPORTED PRIVILEGES")?, + Action::ImportShare => f.write_str("IMPORT SHARE")?, Action::Insert { .. } => f.write_str("INSERT")?, + Action::Manage { manage_type } => write!(f, "MANAGE {manage_type}")?, + Action::ManageReleases => f.write_str("MANAGE RELEASES")?, + Action::ManageVersions => f.write_str("MANAGE VERSIONS")?, + Action::Modify { modify_type } => { + write!(f, "MODIFY")?; + if let Some(modify_type) = modify_type { + write!(f, " {modify_type}")?; + } + } + Action::Monitor { monitor_type } => { + write!(f, "MONITOR")?; + if let Some(monitor_type) = monitor_type { + write!(f, " {monitor_type}")? + } + } + Action::Operate => f.write_str("OPERATE")?, + Action::OverrideShareRestrictions => f.write_str("OVERRIDE SHARE RESTRICTIONS")?, + Action::Ownership => f.write_str("OWNERSHIP")?, + Action::PurchaseDataExchangeListing => f.write_str("PURCHASE DATA EXCHANGE LISTING")?, + Action::Read => f.write_str("READ")?, + Action::ReadSession => f.write_str("READ SESSION")?, Action::References { .. } => f.write_str("REFERENCES")?, + Action::Replicate => f.write_str("REPLICATE")?, + Action::ResolveAll => f.write_str("RESOLVE ALL")?, + Action::Role { role } => write!(f, "ROLE {role}")?, Action::Select { .. } => f.write_str("SELECT")?, Action::Temporary => f.write_str("TEMPORARY")?, Action::Trigger => f.write_str("TRIGGER")?, @@ -5424,6 +6324,188 @@ impl fmt::Display for Action { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `CREATE` privilege. +pub enum ActionCreateObjectType { + Account, + Application, + ApplicationPackage, + ComputePool, + DataExchangeListing, + Database, + ExternalVolume, + FailoverGroup, + Integration, + NetworkPolicy, + OrganiationListing, + ReplicationGroup, + Role, + Schema, + Share, + User, + Warehouse, +} + +impl fmt::Display for ActionCreateObjectType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionCreateObjectType::Account => write!(f, "ACCOUNT"), + ActionCreateObjectType::Application => write!(f, "APPLICATION"), + ActionCreateObjectType::ApplicationPackage => write!(f, "APPLICATION PACKAGE"), + ActionCreateObjectType::ComputePool => write!(f, "COMPUTE POOL"), + ActionCreateObjectType::DataExchangeListing => write!(f, "DATA EXCHANGE LISTING"), + ActionCreateObjectType::Database => write!(f, "DATABASE"), + ActionCreateObjectType::ExternalVolume => write!(f, "EXTERNAL VOLUME"), + ActionCreateObjectType::FailoverGroup => write!(f, "FAILOVER GROUP"), + ActionCreateObjectType::Integration => write!(f, "INTEGRATION"), + ActionCreateObjectType::NetworkPolicy => write!(f, "NETWORK POLICY"), + 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"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `APPLY` privilege. +pub enum ActionApplyType { + AggregationPolicy, + AuthenticationPolicy, + JoinPolicy, + MaskingPolicy, + PackagesPolicy, + PasswordPolicy, + ProjectionPolicy, + RowAccessPolicy, + SessionPolicy, + Tag, +} + +impl fmt::Display for ActionApplyType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionApplyType::AggregationPolicy => write!(f, "AGGREGATION POLICY"), + ActionApplyType::AuthenticationPolicy => write!(f, "AUTHENTICATION POLICY"), + ActionApplyType::JoinPolicy => write!(f, "JOIN POLICY"), + ActionApplyType::MaskingPolicy => write!(f, "MASKING POLICY"), + ActionApplyType::PackagesPolicy => write!(f, "PACKAGES POLICY"), + ActionApplyType::PasswordPolicy => write!(f, "PASSWORD POLICY"), + ActionApplyType::ProjectionPolicy => write!(f, "PROJECTION POLICY"), + ActionApplyType::RowAccessPolicy => write!(f, "ROW ACCESS POLICY"), + ActionApplyType::SessionPolicy => write!(f, "SESSION POLICY"), + ActionApplyType::Tag => write!(f, "TAG"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `EXECUTE` privilege. +pub enum ActionExecuteObjectType { + Alert, + DataMetricFunction, + ManagedAlert, + ManagedTask, + Task, +} + +impl fmt::Display for ActionExecuteObjectType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionExecuteObjectType::Alert => write!(f, "ALERT"), + ActionExecuteObjectType::DataMetricFunction => write!(f, "DATA METRIC FUNCTION"), + ActionExecuteObjectType::ManagedAlert => write!(f, "MANAGED ALERT"), + ActionExecuteObjectType::ManagedTask => write!(f, "MANAGED TASK"), + ActionExecuteObjectType::Task => write!(f, "TASK"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `MANAGE` privilege. +pub enum ActionManageType { + AccountSupportCases, + EventSharing, + Grants, + ListingAutoFulfillment, + OrganizationSupportCases, + UserSupportCases, + Warehouses, +} + +impl fmt::Display for ActionManageType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionManageType::AccountSupportCases => write!(f, "ACCOUNT SUPPORT CASES"), + ActionManageType::EventSharing => write!(f, "EVENT SHARING"), + ActionManageType::Grants => write!(f, "GRANTS"), + ActionManageType::ListingAutoFulfillment => write!(f, "LISTING AUTO FULFILLMENT"), + ActionManageType::OrganizationSupportCases => write!(f, "ORGANIZATION SUPPORT CASES"), + ActionManageType::UserSupportCases => write!(f, "USER SUPPORT CASES"), + ActionManageType::Warehouses => write!(f, "WAREHOUSES"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `MODIFY` privilege. +pub enum ActionModifyType { + LogLevel, + TraceLevel, + SessionLogLevel, + SessionTraceLevel, +} + +impl fmt::Display for ActionModifyType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionModifyType::LogLevel => write!(f, "LOG LEVEL"), + ActionModifyType::TraceLevel => write!(f, "TRACE LEVEL"), + ActionModifyType::SessionLogLevel => write!(f, "SESSION LOG LEVEL"), + ActionModifyType::SessionTraceLevel => write!(f, "SESSION TRACE LEVEL"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `MONITOR` privilege. +pub enum ActionMonitorType { + Execution, + Security, + Usage, +} + +impl fmt::Display for ActionMonitorType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionMonitorType::Execution => write!(f, "EXECUTION"), + ActionMonitorType::Security => write!(f, "SECURITY"), + ActionMonitorType::Usage => write!(f, "USAGE"), + } + } +} + /// The principal that receives the privileges #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -5500,7 +6582,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}") } } } @@ -5515,12 +6597,73 @@ 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 Schemas(Vec), /// Grant privileges on specific sequences Sequences(Vec), /// Grant privileges on specific tables Tables(Vec), + /// Grant privileges on specific views + Views(Vec), + /// Grant privileges on specific warehouses + Warehouses(Vec), + /// Grant privileges on specific integrations + Integrations(Vec), + /// Grant privileges on resource monitors + ResourceMonitors(Vec), + /// Grant privileges on users + Users(Vec), + /// Grant privileges on compute pools + ComputePools(Vec), + /// Grant privileges on connections + Connections(Vec), + /// Grant privileges on failover groups + FailoverGroup(Vec), + /// Grant privileges on replication group + 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 { @@ -5529,12 +6672,24 @@ impl fmt::Display for GrantObjects { GrantObjects::Sequences(sequences) => { write!(f, "SEQUENCE {}", display_comma_separated(sequences)) } + GrantObjects::Databases(databases) => { + write!(f, "DATABASE {}", display_comma_separated(databases)) + } GrantObjects::Schemas(schemas) => { write!(f, "SCHEMA {}", display_comma_separated(schemas)) } GrantObjects::Tables(tables) => { write!(f, "{}", display_comma_separated(tables)) } + GrantObjects::Views(views) => { + write!(f, "VIEW {}", display_comma_separated(views)) + } + GrantObjects::Warehouses(warehouses) => { + write!(f, "WAREHOUSE {}", display_comma_separated(warehouses)) + } + GrantObjects::Integrations(integrations) => { + write!(f, "INTEGRATION {}", display_comma_separated(integrations)) + } GrantObjects::AllSequencesInSchema { schemas } => { write!( f, @@ -5549,53 +6704,189 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } - } - } -} - -/// SQL assignment `foo = expr` as used in SQLUpdate -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct Assignment { - pub target: AssignmentTarget, - pub value: Expr, -} - -impl fmt::Display for Assignment { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} = {}", self.target, self.value) - } -} - -/// Left-hand side of an assignment in an UPDATE statement, -/// e.g. `foo` in `foo = 5` (ColumnName assignment) or -/// `(a, b)` in `(a, b) = (1, 2)` (Tuple assignment). -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum AssignmentTarget { - /// A single column - ColumnName(ObjectName), - /// A tuple of columns - Tuple(Vec), -} - -impl fmt::Display for AssignmentTarget { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - AssignmentTarget::ColumnName(column) => write!(f, "{}", column), - AssignmentTarget::Tuple(columns) => write!(f, "({})", display_comma_separated(columns)), - } - } -} - -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum FunctionArgExpr { - Expr(Expr), - /// Qualified wildcard, e.g. `alias.*` or `schema.table.*`. + 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)) + } + GrantObjects::Users(objects) => { + write!(f, "USER {}", display_comma_separated(objects)) + } + GrantObjects::ComputePools(objects) => { + write!(f, "COMPUTE POOL {}", display_comma_separated(objects)) + } + GrantObjects::Connections(objects) => { + write!(f, "CONNECTION {}", display_comma_separated(objects)) + } + GrantObjects::FailoverGroup(objects) => { + write!(f, "FAILOVER GROUP {}", display_comma_separated(objects)) + } + GrantObjects::ReplicationGroup(objects) => { + write!(f, "REPLICATION GROUP {}", display_comma_separated(objects)) + } + 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(()) + } +} + +/// SQL assignment `foo = expr` as used in SQLUpdate +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Assignment { + pub target: AssignmentTarget, + pub value: Expr, +} + +impl fmt::Display for Assignment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} = {}", self.target, self.value) + } +} + +/// Left-hand side of an assignment in an UPDATE statement, +/// e.g. `foo` in `foo = 5` (ColumnName assignment) or +/// `(a, b)` in `(a, b) = (1, 2)` (Tuple assignment). +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AssignmentTarget { + /// A single column + ColumnName(ObjectName), + /// A tuple of columns + Tuple(Vec), +} + +impl fmt::Display for AssignmentTarget { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AssignmentTarget::ColumnName(column) => write!(f, "{column}"), + AssignmentTarget::Tuple(columns) => write!(f, "({})", display_comma_separated(columns)), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FunctionArgExpr { + Expr(Expr), + /// Qualified wildcard, e.g. `alias.*` or `schema.table.*`. QualifiedWildcard(ObjectName), /// An unqualified `*` Wildcard, @@ -5708,6 +6999,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))] @@ -5784,7 +7134,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 { @@ -5815,8 +7166,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})"), } } } @@ -5837,7 +7188,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() { @@ -5886,18 +7237,23 @@ pub enum FunctionArgumentClause { /// /// [`GROUP_CONCAT`]: https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_group-concat Separator(Value), - /// The json-null-clause to the [`JSON_ARRAY`]/[`JSON_OBJECT`] function in MSSQL. + /// The `ON NULL` clause for some JSON functions. /// - /// [`JSON_ARRAY`]: - /// [`JSON_OBJECT`]: + /// [MSSQL `JSON_ARRAY`](https://learn.microsoft.com/en-us/sql/t-sql/functions/json-array-transact-sql?view=sql-server-ver16) + /// [MSSQL `JSON_OBJECT`](https://learn.microsoft.com/en-us/sql/t-sql/functions/json-object-transact-sql?view=sql-server-ver16>) + /// [PostgreSQL JSON functions](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-PROCESSING) JsonNullClause(JsonNullClause), + /// The `RETURNING` clause for some JSON functions in PostgreSQL + /// + /// [`JSON_OBJECT`](https://www.postgresql.org/docs/current/functions-json.html#:~:text=json_object) + JsonReturningClause(JsonReturningClause), } 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)) @@ -5907,6 +7263,9 @@ impl fmt::Display for FunctionArgumentClause { FunctionArgumentClause::Having(bound) => write!(f, "{bound}"), FunctionArgumentClause::Separator(sep) => write!(f, "SEPARATOR {sep}"), FunctionArgumentClause::JsonNullClause(null_clause) => write!(f, "{null_clause}"), + FunctionArgumentClause::JsonReturningClause(returning_clause) => { + write!(f, "{returning_clause}") + } } } } @@ -5951,6 +7310,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))] @@ -5958,6 +7336,8 @@ pub enum AnalyzeFormat { TEXT, GRAPHVIZ, JSON, + TRADITIONAL, + TREE, } impl fmt::Display for AnalyzeFormat { @@ -5966,6 +7346,8 @@ impl fmt::Display for AnalyzeFormat { AnalyzeFormat::TEXT => "TEXT", AnalyzeFormat::GRAPHVIZ => "GRAPHVIZ", AnalyzeFormat::JSON => "JSON", + AnalyzeFormat::TRADITIONAL => "TRADITIONAL", + AnalyzeFormat::TREE => "TREE", }) } } @@ -6070,6 +7452,7 @@ impl fmt::Display for HavingBoundKind { pub enum ObjectType { Table, View, + MaterializedView, Index, Schema, Database, @@ -6077,6 +7460,8 @@ pub enum ObjectType { Sequence, Stage, Type, + User, + Stream, } impl fmt::Display for ObjectType { @@ -6084,6 +7469,7 @@ impl fmt::Display for ObjectType { f.write_str(match self { ObjectType::Table => "TABLE", ObjectType::View => "VIEW", + ObjectType::MaterializedView => "MATERIALIZED VIEW", ObjectType::Index => "INDEX", ObjectType::Schema => "SCHEMA", ObjectType::Database => "DATABASE", @@ -6091,6 +7477,8 @@ impl fmt::Display for ObjectType { ObjectType::Sequence => "SEQUENCE", ObjectType::Stage => "STAGE", ObjectType::Type => "TYPE", + ObjectType::User => "USER", + ObjectType::Stream => "STREAM", }) } } @@ -6334,17 +7722,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, @@ -6365,10 +7765,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))] @@ -6383,6 +7827,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))] @@ -6397,7 +7905,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}"), } } } @@ -6617,7 +8125,7 @@ impl fmt::Display for CopyTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use CopyTarget::*; match self { - Stdin { .. } => write!(f, "STDIN"), + Stdin => write!(f, "STDIN"), Stdout => write!(f, "STDOUT"), File { filename } => write!(f, "'{}'", value::escape_single_quote_string(filename)), Program { command } => write!( @@ -6694,40 +8202,281 @@ 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), + /// ADDQUOTES + AddQuotes, + /// ALLOWOVERWRITE + AllowOverwrite, /// BINARY Binary, + /// BLANKSASNULL + BlankAsNull, + /// BZIP2 + Bzip2, + /// CLEANPATH + CleanPath, + /// COMPUPDATE [ PRESET | { ON | TRUE } | { OFF | FALSE } ] + CompUpdate { preset: bool, enabled: Option }, + /// CSV ... + Csv(Vec), + /// DATEFORMAT \[ AS \] {'dateformat_string' | 'auto' } + DateFormat(Option), /// DELIMITER \[ AS \] 'delimiter_character' Delimiter(char), + /// EMPTYASNULL + EmptyAsNull, + /// ENCRYPTED \[ AUTO \] + Encrypted { auto: bool }, + /// ESCAPE + Escape, + /// EXTENSION 'extension-name' + Extension(String), + /// FIXEDWIDTH \[ AS \] 'fixedwidth-spec' + FixedWidth(String), + /// GZIP + Gzip, + /// HEADER + Header, + /// IAM_ROLE { DEFAULT | 'arn:aws:iam::123456789:role/role1' } + IamRole(IamRoleKind), + /// IGNOREHEADER \[ AS \] number_rows + IgnoreHeader(u64), + /// JSON + Json, + /// MANIFEST \[ VERBOSE \] + Manifest { verbose: bool }, + /// MAXFILESIZE \[ AS \] max-size \[ MB | GB \] + MaxFileSize(FileSize), /// NULL \[ AS \] 'null_string' Null(String), - /// CSV ... - Csv(Vec), + /// PARALLEL [ { ON | TRUE } | { OFF | FALSE } ] + Parallel(Option), + /// PARQUET + Parquet, + /// PARTITION BY ( column_name [, ... ] ) \[ INCLUDE \] + PartitionBy(UnloadPartitionBy), + /// REGION \[ AS \] 'aws-region' } + Region(String), + /// REMOVEQUOTES + RemoveQuotes, + /// ROWGROUPSIZE \[ AS \] size \[ MB | GB \] + RowGroupSize(FileSize), + /// STATUPDATE [ { ON | TRUE } | { OFF | FALSE } ] + StatUpdate(Option), + /// TIMEFORMAT \[ AS \] {'timeformat_string' | 'auto' | 'epochsecs' | 'epochmillisecs' } + TimeFormat(Option), + /// TRUNCATECOLUMNS + TruncateColumns, + /// ZSTD + Zstd, } 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(()) + } + AddQuotes => write!(f, "ADDQUOTES"), + AllowOverwrite => write!(f, "ALLOWOVERWRITE"), Binary => write!(f, "BINARY"), + BlankAsNull => write!(f, "BLANKSASNULL"), + Bzip2 => write!(f, "BZIP2"), + CleanPath => write!(f, "CLEANPATH"), + CompUpdate { preset, enabled } => { + write!(f, "COMPUPDATE")?; + if *preset { + write!(f, " PRESET")?; + } else if let Some(enabled) = enabled { + write!( + f, + "{}", + match enabled { + true => " TRUE", + false => " FALSE", + } + )?; + } + Ok(()) + } + 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"), + Encrypted { auto } => write!(f, "ENCRYPTED{}", if *auto { " AUTO" } else { "" }), + Escape => write!(f, "ESCAPE"), + Extension(ext) => write!(f, "EXTENSION '{}'", value::escape_single_quote_string(ext)), + FixedWidth(spec) => write!( + f, + "FIXEDWIDTH '{}'", + value::escape_single_quote_string(spec) + ), + Gzip => write!(f, "GZIP"), + Header => write!(f, "HEADER"), + IamRole(role) => write!(f, "IAM_ROLE {role}"), + IgnoreHeader(num_rows) => write!(f, "IGNOREHEADER {num_rows}"), + Json => write!(f, "JSON"), + Manifest { verbose } => write!(f, "MANIFEST{}", if *verbose { " VERBOSE" } else { "" }), + MaxFileSize(file_size) => write!(f, "MAXFILESIZE {file_size}"), Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)), - Csv(opts) => write!(f, "CSV {}", display_separated(opts, " ")), - } - } -} - -/// A `CSV` option in `COPY` statement before PostgreSQL version 9.0. -/// -/// -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum CopyLegacyCsvOption { + Parallel(enabled) => { + write!( + f, + "PARALLEL{}", + match enabled { + Some(true) => " TRUE", + Some(false) => " FALSE", + _ => "", + } + ) + } + Parquet => write!(f, "PARQUET"), + PartitionBy(p) => write!(f, "{p}"), + Region(region) => write!(f, "REGION '{}'", value::escape_single_quote_string(region)), + RemoveQuotes => write!(f, "REMOVEQUOTES"), + RowGroupSize(file_size) => write!(f, "ROWGROUPSIZE {file_size}"), + StatUpdate(enabled) => { + write!( + f, + "STATUPDATE{}", + match enabled { + Some(true) => " TRUE", + Some(false) => " FALSE", + _ => "", + } + ) + } + TimeFormat(fmt) => { + write!(f, "TIMEFORMAT")?; + if let Some(fmt) = fmt { + write!(f, " '{}'", value::escape_single_quote_string(fmt))?; + } + Ok(()) + } + TruncateColumns => write!(f, "TRUNCATECOLUMNS"), + Zstd => write!(f, "ZSTD"), + } + } +} + +/// ```sql +/// SIZE \[ MB | GB \] +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct FileSize { + pub size: Value, + pub unit: Option, +} + +impl fmt::Display for FileSize { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.size)?; + if let Some(unit) = &self.unit { + write!(f, " {unit}")?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FileSizeUnit { + MB, + GB, +} + +impl fmt::Display for FileSizeUnit { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FileSizeUnit::MB => write!(f, "MB"), + FileSizeUnit::GB => write!(f, "GB"), + } + } +} + +/// Specifies the partition keys for the unload operation +/// +/// ```sql +/// PARTITION BY ( column_name [, ... ] ) [ INCLUDE ] +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct UnloadPartitionBy { + pub columns: Vec, + pub include: bool, +} + +impl fmt::Display for UnloadPartitionBy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "PARTITION BY ({}){}", + display_comma_separated(&self.columns), + if self.include { " INCLUDE" } else { "" } + ) + } +} + +/// 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}'"), + } + } +} + +/// A `CSV` option in `COPY` statement before PostgreSQL version 9.0. +/// +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CopyLegacyCsvOption { /// HEADER Header, /// QUOTE \[ AS \] 'quote_character' @@ -6945,6 +8694,47 @@ impl Display for MergeClause { } } +/// A Output Clause in the end of a 'MERGE' Statement +/// +/// Example: +/// OUTPUT $action, deleted.* INTO dbo.temp_products; +/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-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 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 { + 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) + } + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7022,30 +8812,30 @@ impl fmt::Display for FlushLocation { } } -/// Optional context modifier for statements that can be or `LOCAL`, or `SESSION`. +/// Optional context modifier for statements that can be or `LOCAL`, `GLOBAL`, or `SESSION`. #[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 ContextModifier { - /// No context defined. Each dialect defines the default in this scenario. - None, /// `LOCAL` identifier, usually related to transactional states. Local, /// `SESSION` identifier Session, + /// `GLOBAL` identifier + Global, } impl fmt::Display for ContextModifier { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::None => { - write!(f, "") - } Self::Local => { - write!(f, " LOCAL") + write!(f, "LOCAL ") } Self::Session => { - write!(f, " SESSION") + write!(f, "SESSION ") + } + Self::Global => { + write!(f, "GLOBAL ") } } } @@ -7244,7 +9034,8 @@ impl fmt::Display for FunctionDeterminismSpecifier { /// where within the statement, the body shows up. /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 -/// [Postgres]: https://www.postgresql.org/docs/15/sql-createfunction.html +/// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html +/// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -7273,6 +9064,22 @@ pub enum CreateFunctionBody { /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 AsAfterOptions(Expr), + /// Function body with statements before the `RETURN` keyword. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION my_scalar_udf(a INT, b INT) + /// RETURNS INT + /// AS + /// BEGIN + /// DECLARE c INT; + /// SET c = a + b; + /// RETURN c; + /// END + /// ``` + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql + AsBeginEnd(BeginEndStatements), /// Function body expression using the 'RETURN' keyword. /// /// Example: @@ -7282,8 +9089,32 @@ pub enum CreateFunctionBody { /// RETURN a + b; /// ``` /// - /// [Postgres]: https://www.postgresql.org/docs/current/sql-createfunction.html + /// [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)] @@ -7588,27 +9419,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, ...)` @@ -7647,12 +9471,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 } } } @@ -7663,6 +9487,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))] @@ -7672,18 +9513,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}"), } } } @@ -7749,7 +9584,7 @@ where /// ```sql /// EXPLAIN (ANALYZE, VERBOSE TRUE, FORMAT TEXT) SELECT * FROM my_table; /// -/// VACCUM (VERBOSE, ANALYZE ON, PARALLEL 10) my_table; +/// VACUUM (VERBOSE, ANALYZE ON, PARALLEL 10) my_table; /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -7863,209 +9698,1159 @@ 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}")?; + } + 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(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ShowObjects { + pub terse: bool, + pub show_options: ShowStatementOptions, +} + +/// MSSQL's json null clause +/// +/// ```plaintext +/// ::= +/// NULL ON NULL +/// | ABSENT ON NULL +/// ``` +/// +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum JsonNullClause { + NullOnNull, + AbsentOnNull, +} + +impl Display for JsonNullClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JsonNullClause::NullOnNull => write!(f, "NULL ON NULL"), + JsonNullClause::AbsentOnNull => write!(f, "ABSENT ON NULL"), + } + } +} + +/// PostgreSQL JSON function RETURNING clause +/// +/// Example: +/// ```sql +/// JSON_OBJECT('a': 1 RETURNING jsonb) +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct JsonReturningClause { + pub data_type: DataType, +} + +impl Display for JsonReturningClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "RETURNING {}", self.data_type) + } +} + +/// rename object definition +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct RenameTable { + pub old_name: ObjectName, + pub new_name: ObjectName, +} + +impl fmt::Display for RenameTable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} TO {}", self.old_name, self.new_name)?; + Ok(()) + } +} + +/// Represents the referenced table in an `INSERT INTO` 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 TableObject { + /// Table specified by name. + /// Example: + /// ```sql + /// INSERT INTO my_table + /// ``` + TableName(#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] ObjectName), + + /// Table specified as a function. + /// Example: + /// ```sql + /// INSERT INTO TABLE FUNCTION remote('localhost', default.simple_table) + /// ``` + /// [Clickhouse](https://clickhouse.com/docs/en/sql-reference/table-functions) + TableFunction(Function), +} + +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}"), + } + } +} + +/// Represents a SET SESSION AUTHORIZATION 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 SetSessionAuthorizationParam { + pub scope: ContextModifier, + pub kind: SetSessionAuthorizationParamKind, +} + +impl fmt::Display for SetSessionAuthorizationParam { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.kind) + } +} + +/// Represents the parameter kind for SET SESSION AUTHORIZATION +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SetSessionAuthorizationParamKind { + /// Default authorization + Default, + + /// User name + User(Ident), +} + +impl fmt::Display for SetSessionAuthorizationParamKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SetSessionAuthorizationParamKind::Default => write!(f, "DEFAULT"), + SetSessionAuthorizationParamKind::User(name) => write!(f, "{}", name), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SetSessionParamKind { + Generic(SetSessionParamGeneric), + IdentityInsert(SetSessionParamIdentityInsert), + Offsets(SetSessionParamOffsets), + Statistics(SetSessionParamStatistics), +} + +impl fmt::Display for SetSessionParamKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SetSessionParamKind::Generic(x) => write!(f, "{x}"), + SetSessionParamKind::IdentityInsert(x) => write!(f, "{x}"), + SetSessionParamKind::Offsets(x) => write!(f, "{x}"), + SetSessionParamKind::Statistics(x) => write!(f, "{x}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionParamGeneric { + pub names: Vec, + pub value: String, +} + +impl fmt::Display for SetSessionParamGeneric { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", display_comma_separated(&self.names), self.value) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionParamIdentityInsert { + pub obj: ObjectName, + pub value: SessionParamValue, +} + +impl fmt::Display for SetSessionParamIdentityInsert { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "IDENTITY_INSERT {} {}", self.obj, self.value) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionParamOffsets { + pub keywords: Vec, + pub value: SessionParamValue, +} + +impl fmt::Display for SetSessionParamOffsets { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "OFFSETS {} {}", + display_comma_separated(&self.keywords), + self.value + ) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionParamStatistics { + pub topic: SessionParamStatsTopic, + pub value: SessionParamValue, +} + +impl fmt::Display for SetSessionParamStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "STATISTICS {} {}", self.topic, self.value) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SessionParamStatsTopic { + IO, + Profile, + Time, + Xml, +} + +impl fmt::Display for SessionParamStatsTopic { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SessionParamStatsTopic::IO => write!(f, "IO"), + SessionParamStatsTopic::Profile => write!(f, "PROFILE"), + SessionParamStatsTopic::Time => write!(f, "TIME"), + SessionParamStatsTopic::Xml => write!(f, "XML"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SessionParamValue { + On, + Off, +} + +impl fmt::Display for SessionParamValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SessionParamValue::On => write!(f, "ON"), + SessionParamValue::Off => write!(f, "OFF"), + } + } +} + +/// Snowflake StorageSerializationPolicy for Iceberg Tables +/// ```sql +/// [ STORAGE_SERIALIZATION_POLICY = { COMPATIBLE | OPTIMIZED } ] +/// ``` +/// +/// +#[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 StorageSerializationPolicy { + Compatible, + Optimized, +} + +impl Display for StorageSerializationPolicy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StorageSerializationPolicy::Compatible => write!(f, "COMPATIBLE"), + StorageSerializationPolicy::Optimized => write!(f, "OPTIMIZED"), + } + } +} + +/// 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))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CopyIntoSnowflakeKind { + /// Loads data from files to a table + /// See: + Table, + /// Unloads data from a table or query to external files + /// See: + Location, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct PrintStatement { + pub message: Box, +} + +impl fmt::Display for PrintStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PRINT {}", self.message) + } +} + +/// Represents a `Return` statement. +/// +/// [MsSql triggers](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql) +/// [MsSql functions](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-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 ReturnStatement { + pub value: Option, +} + +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}"), + None => write!(f, "RETURN"), + } + } +} + +/// Variants of a `RETURN` 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 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(()) + } +} + +/// Modifies the properties of a user +/// +/// Syntax: +/// ```sql +/// ALTER USER [ IF EXISTS ] [ ] [ OPTIONS ] +/// ``` +/// +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/alter-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 AlterUser { + pub if_exists: bool, + pub name: Ident, + /// The following fields are Snowflake-specific: + pub rename_to: Option, + pub reset_password: bool, + pub abort_all_queries: bool, + pub add_role_delegation: Option, + pub remove_role_delegation: Option, + pub enroll_mfa: bool, + pub set_default_mfa_method: Option, + pub remove_mfa_method: Option, + pub modify_mfa_method: Option, + pub add_mfa_method_otp: Option, + pub set_policy: Option, + pub unset_policy: Option, + pub set_tag: KeyValueOptions, + pub unset_tag: Vec, + pub set_props: KeyValueOptions, + pub unset_props: Vec, +} + +/// ```sql +/// ALTER USER [ IF EXISTS ] [ ] ADD DELEGATED AUTHORIZATION OF ROLE TO SECURITY INTEGRATION +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterUserAddRoleDelegation { + pub role: Ident, + pub integration: Ident, +} + +/// ```sql +/// ALTER USER [ IF EXISTS ] [ ] REMOVE DELEGATED { AUTHORIZATION OF ROLE | AUTHORIZATIONS } FROM SECURITY INTEGRATION +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterUserRemoveRoleDelegation { + pub role: Option, + pub integration: Ident, +} + +/// ```sql +/// ADD MFA METHOD OTP [ COUNT = number ] +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterUserAddMfaMethodOtp { + pub count: Option, +} + +/// ```sql +/// ALTER USER [ IF EXISTS ] [ ] MODIFY MFA METHOD SET COMMENT = '' +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterUserModifyMfaMethod { + pub method: MfaMethodKind, + pub comment: String, +} + +/// Types of MFA methods +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MfaMethodKind { + PassKey, + Totp, + Duo, +} + +impl fmt::Display for MfaMethodKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MfaMethodKind::PassKey => write!(f, "PASSKEY"), + MfaMethodKind::Totp => write!(f, "TOTP"), + MfaMethodKind::Duo => write!(f, "DUO"), + } + } +} + +/// ```sql +/// ALTER USER [ IF EXISTS ] [ ] SET { AUTHENTICATION | PASSWORD | SESSION } POLICY +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterUserSetPolicy { + pub policy_kind: UserPolicyKind, + pub policy: Ident, +} + +/// Types of user-based policies +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum UserPolicyKind { + Authentication, + Password, + Session, +} + +impl fmt::Display for UserPolicyKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + UserPolicyKind::Authentication => write!(f, "AUTHENTICATION"), + UserPolicyKind::Password => write!(f, "PASSWORD"), + UserPolicyKind::Session => write!(f, "SESSION"), + } + } +} + +impl fmt::Display for AlterUser { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ALTER")?; + write!(f, " USER")?; + if self.if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {}", self.name)?; + if let Some(new_name) = &self.rename_to { + write!(f, " RENAME TO {new_name}")?; + } + if self.reset_password { + write!(f, " RESET PASSWORD")?; + } + if self.abort_all_queries { + write!(f, " ABORT ALL QUERIES")?; + } + if let Some(role_delegation) = &self.add_role_delegation { + let role = &role_delegation.role; + let integration = &role_delegation.integration; + write!( + f, + " ADD DELEGATED AUTHORIZATION OF ROLE {role} TO SECURITY INTEGRATION {integration}" + )?; + } + if let Some(role_delegation) = &self.remove_role_delegation { + write!(f, " REMOVE DELEGATED")?; + match &role_delegation.role { + Some(role) => write!(f, " AUTHORIZATION OF ROLE {role}")?, + None => write!(f, " AUTHORIZATIONS")?, + } + let integration = &role_delegation.integration; + write!(f, " FROM SECURITY INTEGRATION {integration}")?; + } + if self.enroll_mfa { + write!(f, " ENROLL MFA")?; + } + if let Some(method) = &self.set_default_mfa_method { + write!(f, " SET DEFAULT_MFA_METHOD {method}")? + } + if let Some(method) = &self.remove_mfa_method { + write!(f, " REMOVE MFA METHOD {method}")?; + } + if let Some(modify) = &self.modify_mfa_method { + let method = &modify.method; + let comment = &modify.comment; + write!( + f, + " MODIFY MFA METHOD {method} SET COMMENT '{}'", + value::escape_single_quote_string(comment) + )?; + } + if let Some(add_mfa_method_otp) = &self.add_mfa_method_otp { + write!(f, " ADD MFA METHOD OTP")?; + if let Some(count) = &add_mfa_method_otp.count { + write!(f, " COUNT = {count}")?; + } } - if let Some(parent_name) = &self.parent_name { - write!(f, " {}", parent_name)?; + if let Some(policy) = &self.set_policy { + let policy_kind = &policy.policy_kind; + let name = &policy.policy; + write!(f, " SET {policy_kind} POLICY {name}")?; + } + if let Some(policy_kind) = &self.unset_policy { + write!(f, " UNSET {policy_kind} POLICY")?; + } + if !self.set_tag.options.is_empty() { + write!(f, " SET TAG {}", self.set_tag)?; + } + if !self.unset_tag.is_empty() { + write!(f, " UNSET TAG {}", display_comma_separated(&self.unset_tag))?; + } + let has_props = !self.set_props.options.is_empty(); + if has_props { + write!(f, " SET")?; + write!(f, " {}", &self.set_props)?; + } + if !self.unset_props.is_empty() { + write!(f, " UNSET {}", display_comma_separated(&self.unset_props))?; } Ok(()) } } -/// MSSQL's json null clause -/// -/// ```plaintext -/// ::= -/// NULL ON NULL -/// | ABSENT ON NULL -/// ``` -/// -/// +/// 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 JsonNullClause { - NullOnNull, - AbsentOnNull, +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), } -impl Display for JsonNullClause { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +#[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 { - JsonNullClause::NullOnNull => write!(f, "NULL ON NULL"), - JsonNullClause::AbsentOnNull => write!(f, "ABSENT ON NULL"), + CreateTableLikeDefaults::Including => write!(f, "INCLUDING DEFAULTS"), + CreateTableLikeDefaults::Excluding => write!(f, "EXCLUDING DEFAULTS"), } } } -/// rename object definition #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct RenameTable { - pub old_name: ObjectName, - pub new_name: ObjectName, +pub struct CreateTableLike { + pub name: ObjectName, + pub defaults: Option, } -impl fmt::Display for RenameTable { +impl fmt::Display for CreateTableLike { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} TO {}", self.old_name, self.new_name)?; + write!(f, "LIKE {}", self.name)?; + if let Some(defaults) = &self.defaults { + write!(f, " {defaults}")?; + } Ok(()) } } -/// Represents the referenced table in an `INSERT INTO` statement -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +/// 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 TableObject { - /// Table specified by name. - /// Example: - /// ```sql - /// INSERT INTO my_table - /// ``` - TableName(#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] ObjectName), - - /// Table specified as a function. - /// Example: - /// ```sql - /// INSERT INTO TABLE FUNCTION remote('localhost', default.simple_table) - /// ``` - /// [Clickhouse](https://clickhouse.com/docs/en/sql-reference/table-functions) - TableFunction(Function), +pub enum RefreshModeKind { + Auto, + Full, + Incremental, } -impl fmt::Display for TableObject { +impl fmt::Display for RefreshModeKind { 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), + RefreshModeKind::Auto => write!(f, "AUTO"), + RefreshModeKind::Full => write!(f, "FULL"), + RefreshModeKind::Incremental => write!(f, "INCREMENTAL"), } } } -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +/// 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 SetSessionParamKind { - Generic(SetSessionParamGeneric), - IdentityInsert(SetSessionParamIdentityInsert), - Offsets(SetSessionParamOffsets), - Statistics(SetSessionParamStatistics), +pub enum InitializeKind { + OnCreate, + OnSchedule, } -impl fmt::Display for SetSessionParamKind { +impl fmt::Display for InitializeKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - SetSessionParamKind::Generic(x) => write!(f, "{x}"), - SetSessionParamKind::IdentityInsert(x) => write!(f, "{x}"), - SetSessionParamKind::Offsets(x) => write!(f, "{x}"), - SetSessionParamKind::Statistics(x) => write!(f, "{x}"), + 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 SetSessionParamGeneric { - pub names: Vec, - pub value: String, +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 SetSessionParamGeneric { +impl fmt::Display for VacuumStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} {}", display_comma_separated(&self.names), self.value) + 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(()) } } +/// Variants of the RESET 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 SetSessionParamIdentityInsert { - pub obj: ObjectName, - pub value: SessionParamValue, -} +pub enum Reset { + /// Resets all session parameters to their default values. + ALL, -impl fmt::Display for SetSessionParamIdentityInsert { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "IDENTITY_INSERT {} {}", self.obj, self.value) - } + /// Resets a specific session parameter to its default value. + ConfigurationParameter(ObjectName), } +/// Resets a session parameter to its default value. +/// ```sql +/// RESET { ALL | } +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct SetSessionParamOffsets { - pub keywords: Vec, - pub value: SessionParamValue, +pub struct ResetStatement { + pub reset: Reset, } -impl fmt::Display for SetSessionParamOffsets { +impl fmt::Display for ResetStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "OFFSETS {} {}", - display_comma_separated(&self.keywords), - self.value - ) + match &self.reset { + Reset::ALL => write!(f, "RESET ALL"), + Reset::ConfigurationParameter(param) => write!(f, "RESET {}", param), + } } } -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct SetSessionParamStatistics { - pub topic: SessionParamStatsTopic, - pub value: SessionParamValue, +impl From for Statement { + fn from(s: Set) -> Self { + Self::Set(s) + } } -impl fmt::Display for SetSessionParamStatistics { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "STATISTICS {} {}", self.topic, self.value) +impl From for Statement { + fn from(q: Query) -> Self { + Box::new(q).into() } } -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum SessionParamStatsTopic { - IO, - Profile, - Time, - Xml, +impl From> for Statement { + fn from(q: Box) -> Self { + Self::Query(q) + } } -impl fmt::Display for SessionParamStatsTopic { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - SessionParamStatsTopic::IO => write!(f, "IO"), - SessionParamStatsTopic::Profile => write!(f, "PROFILE"), - SessionParamStatsTopic::Time => write!(f, "TIME"), - SessionParamStatsTopic::Xml => write!(f, "XML"), - } +impl From for Statement { + fn from(i: Insert) -> Self { + Self::Insert(i) } } -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum SessionParamValue { - On, - Off, +impl From for Statement { + fn from(u: Update) -> Self { + Self::Update(u) + } } -impl fmt::Display for SessionParamValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - SessionParamValue::On => write!(f, "ON"), - SessionParamValue::Off => write!(f, "OFF"), - } +impl From for Statement { + fn from(cv: CreateView) -> Self { + Self::CreateView(cv) + } +} + +impl From for Statement { + fn from(cr: CreateRole) -> Self { + Self::CreateRole(cr) + } +} + +impl From for Statement { + fn from(at: AlterTable) -> Self { + Self::AlterTable(at) + } +} + +impl From for Statement { + fn from(df: DropFunction) -> Self { + Self::DropFunction(df) + } +} + +impl From for Statement { + fn from(ce: CreateExtension) -> Self { + Self::CreateExtension(ce) + } +} + +impl From for Statement { + fn from(de: DropExtension) -> Self { + Self::DropExtension(de) + } +} + +impl From for Statement { + fn from(c: CaseStatement) -> Self { + Self::Case(c) + } +} + +impl From for Statement { + fn from(i: IfStatement) -> Self { + Self::If(i) + } +} + +impl From for Statement { + fn from(w: WhileStatement) -> Self { + Self::While(w) + } +} + +impl From for Statement { + fn from(r: RaiseStatement) -> Self { + Self::Raise(r) + } +} + +impl From for Statement { + fn from(f: Function) -> Self { + Self::Call(f) + } +} + +impl From for Statement { + fn from(o: OpenStatement) -> Self { + Self::Open(o) + } +} + +impl From for Statement { + fn from(d: Delete) -> Self { + Self::Delete(d) + } +} + +impl From for Statement { + fn from(c: CreateTable) -> Self { + Self::CreateTable(c) + } +} + +impl From for Statement { + fn from(c: CreateIndex) -> Self { + Self::CreateIndex(c) + } +} + +impl From for Statement { + fn from(c: CreateServerStatement) -> Self { + Self::CreateServer(c) + } +} + +impl From for Statement { + fn from(c: CreateConnector) -> Self { + Self::CreateConnector(c) + } +} + +impl From for Statement { + fn from(a: AlterSchema) -> Self { + Self::AlterSchema(a) + } +} + +impl From for Statement { + fn from(a: AlterType) -> Self { + Self::AlterType(a) + } +} + +impl From for Statement { + fn from(d: DropDomain) -> Self { + Self::DropDomain(d) + } +} + +impl From for Statement { + fn from(s: ShowCharset) -> Self { + Self::ShowCharset(s) + } +} + +impl From for Statement { + fn from(s: ShowObjects) -> Self { + Self::ShowObjects(s) + } +} + +impl From for Statement { + fn from(u: Use) -> Self { + Self::Use(u) + } +} + +impl From for Statement { + fn from(c: CreateFunction) -> Self { + Self::CreateFunction(c) + } +} + +impl From for Statement { + fn from(c: CreateTrigger) -> Self { + Self::CreateTrigger(c) + } +} + +impl From for Statement { + fn from(d: DropTrigger) -> Self { + Self::DropTrigger(d) + } +} + +impl From for Statement { + fn from(d: DenyStatement) -> Self { + Self::Deny(d) + } +} + +impl From for Statement { + fn from(c: CreateDomain) -> Self { + Self::CreateDomain(c) + } +} + +impl From for Statement { + fn from(r: RenameTable) -> Self { + vec![r].into() + } +} + +impl From> for Statement { + fn from(r: Vec) -> Self { + Self::RenameTable(r) + } +} + +impl From for Statement { + fn from(p: PrintStatement) -> Self { + Self::Print(p) + } +} + +impl From for Statement { + fn from(r: ReturnStatement) -> Self { + Self::Return(r) + } +} + +impl From for Statement { + fn from(e: ExportData) -> Self { + Self::ExportData(e) + } +} + +impl From for Statement { + fn from(c: CreateUser) -> Self { + Self::CreateUser(c) + } +} + +impl From for Statement { + fn from(v: VacuumStatement) -> Self { + Self::Vacuum(v) + } +} + +impl From for Statement { + fn from(r: ResetStatement) -> Self { + Self::Reset(r) } } #[cfg(test)] mod tests { + use crate::tokenizer::Location; + use super::*; #[test] @@ -8163,9 +10948,9 @@ mod tests { #[test] fn test_interval_display() { let interval = Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "123:45.67", - )))), + value: Box::new(Expr::Value( + Value::SingleQuotedString(String::from("123:45.67")).with_empty_span(), + )), leading_field: Some(DateTimeField::Minute), leading_precision: Some(10), last_field: Some(DateTimeField::Second), @@ -8177,7 +10962,9 @@ mod tests { ); let interval = Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("5")))), + value: Box::new(Expr::Value( + Value::SingleQuotedString(String::from("5")).with_empty_span(), + )), leading_field: Some(DateTimeField::Second), leading_precision: Some(1), last_field: None, @@ -8359,4 +11146,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 1f9a6b82b..58c401f7d 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -33,41 +33,61 @@ use super::display_separated; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum UnaryOperator { + /// `@-@` Length or circumference (PostgreSQL/Redshift geometric operator) + /// see + AtDashAt, + /// Unary logical not operator: e.g. `! false` (Hive-specific) + BangNot, + /// Bitwise Not, e.g. `~9` + BitwiseNot, + /// `@@` Center (PostgreSQL/Redshift geometric operator) + /// see + DoubleAt, + /// `#` Number of points in path or polygon (PostgreSQL/Redshift geometric operator) + /// see + Hash, /// Plus, e.g. `+9` Plus, /// Minus, e.g. `-9` Minus, /// Not, e.g. `NOT(true)` Not, - /// Bitwise Not, e.g. `~9` (PostgreSQL-specific) - PGBitwiseNot, - /// Square root, e.g. `|/9` (PostgreSQL-specific) - PGSquareRoot, + /// Absolute value, e.g. `@ -9` (PostgreSQL-specific) + PGAbs, /// Cube root, e.g. `||/27` (PostgreSQL-specific) PGCubeRoot, /// Factorial, e.g. `9!` (PostgreSQL-specific) PGPostfixFactorial, /// Factorial, e.g. `!!9` (PostgreSQL-specific) PGPrefixFactorial, - /// Absolute value, e.g. `@ -9` (PostgreSQL-specific) - PGAbs, - /// Unary logical not operator: e.g. `! false` (Hive-specific) - BangNot, + /// Square root, e.g. `|/9` (PostgreSQL-specific) + PGSquareRoot, + /// `?-` Is horizontal? (PostgreSQL/Redshift geometric operator) + /// see + QuestionDash, + /// `?|` Is vertical? (PostgreSQL/Redshift geometric operator) + /// see + QuestionPipe, } impl fmt::Display for UnaryOperator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(match self { - UnaryOperator::Plus => "+", + UnaryOperator::AtDashAt => "@-@", + UnaryOperator::BangNot => "!", + UnaryOperator::BitwiseNot => "~", + UnaryOperator::DoubleAt => "@@", + UnaryOperator::Hash => "#", UnaryOperator::Minus => "-", UnaryOperator::Not => "NOT", - UnaryOperator::PGBitwiseNot => "~", - UnaryOperator::PGSquareRoot => "|/", + UnaryOperator::PGAbs => "@", UnaryOperator::PGCubeRoot => "||/", UnaryOperator::PGPostfixFactorial => "!", UnaryOperator::PGPrefixFactorial => "!!", - UnaryOperator::PGAbs => "@", - UnaryOperator::BangNot => "!", + UnaryOperator::PGSquareRoot => "|/", + UnaryOperator::Plus => "+", + UnaryOperator::QuestionDash => "?-", + UnaryOperator::QuestionPipe => "?|", }) } } @@ -119,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) @@ -253,6 +278,57 @@ pub enum BinaryOperator { /// Specifies a test for an overlap between two datetime periods: /// Overlaps, + /// `##` Point of closest proximity (PostgreSQL/Redshift geometric operator) + /// See + DoubleHash, + /// `<->` Distance between (PostgreSQL/Redshift geometric operator) + /// See + LtDashGt, + /// `&<` Overlaps to left? (PostgreSQL/Redshift geometric operator) + /// See + AndLt, + /// `&>` Overlaps to right? (PostgreSQL/Redshift geometric operator) + /// See + AndGt, + /// `<<|` Is strictly below? (PostgreSQL/Redshift geometric operator) + /// See + LtLtPipe, + /// `|>>` Is strictly above? (PostgreSQL/Redshift geometric operator) + /// See + PipeGtGt, + /// `&<|` Does not extend above? (PostgreSQL/Redshift geometric operator) + /// See + AndLtPipe, + /// `|&>` Does not extend below? (PostgreSQL/Redshift geometric operator) + /// See + PipeAndGt, + /// `<^` Is below? (PostgreSQL/Redshift geometric operator) + /// See + LtCaret, + /// `>^` Is above? (PostgreSQL/Redshift geometric operator) + /// See + GtCaret, + /// `?#` Intersects? (PostgreSQL/Redshift geometric operator) + /// See + QuestionHash, + /// `?-` Is horizontal? (PostgreSQL/Redshift geometric operator) + /// See + QuestionDash, + /// `?-|` Is perpendicular? (PostgreSQL/Redshift geometric operator) + /// See + QuestionDashPipe, + /// `?||` Are Parallel? (PostgreSQL/Redshift geometric operator) + /// See + QuestionDoublePipe, + /// `@` Contained or on? (PostgreSQL/Redshift geometric operator) + /// See + At, + /// `~=` Same as? (PostgreSQL/Redshift geometric operator) + /// See + TildeEq, + /// ':=' Assignment Operator + /// See + Assignment, } impl fmt::Display for BinaryOperator { @@ -279,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("<<"), @@ -310,6 +388,23 @@ impl fmt::Display for BinaryOperator { write!(f, "OPERATOR({})", display_separated(idents, ".")) } BinaryOperator::Overlaps => f.write_str("OVERLAPS"), + BinaryOperator::DoubleHash => f.write_str("##"), + BinaryOperator::LtDashGt => f.write_str("<->"), + BinaryOperator::AndLt => f.write_str("&<"), + BinaryOperator::AndGt => f.write_str("&>"), + BinaryOperator::LtLtPipe => f.write_str("<<|"), + BinaryOperator::PipeGtGt => f.write_str("|>>"), + BinaryOperator::AndLtPipe => f.write_str("&<|"), + BinaryOperator::PipeAndGt => f.write_str("|&>"), + BinaryOperator::LtCaret => f.write_str("<^"), + BinaryOperator::GtCaret => f.write_str(">^"), + BinaryOperator::QuestionHash => f.write_str("?#"), + BinaryOperator::QuestionDash => f.write_str("?-"), + BinaryOperator::QuestionDashPipe => f.write_str("?-|"), + BinaryOperator::QuestionDoublePipe => f.write_str("?||"), + BinaryOperator::At => f.write_str("@"), + BinaryOperator::TildeEq => f.write_str("~="), + BinaryOperator::Assignment => f.write_str(":="), } } } diff --git a/src/ast/query.rs b/src/ast/query.rs index e7020ae23..33c92614f 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}, }; @@ -43,14 +44,8 @@ pub struct Query { pub body: Box, /// ORDER BY pub order_by: Option, - /// `LIMIT { | ALL }` - pub limit: Option, - - /// `LIMIT { } BY { ,,... } }` - pub limit_by: Vec, - - /// `OFFSET [ { ROW | ROWS } ]` - pub offset: Option, + /// `LIMIT ... OFFSET ... | LIMIT , ` + pub limit_clause: Option, /// `FETCH { FIRST | NEXT } [ PERCENT ] { ROW | ROWS } | { ONLY | WITH TIES }` pub fetch: Option, /// `FOR { UPDATE | SHARE } [ OF table_name ] [ SKIP LOCKED | NOWAIT ]` @@ -68,40 +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) = self.limit { - write!(f, " LIMIT {limit}")?; - } - if let Some(ref offset) = self.offset { - write!(f, " {offset}")?; - } - if !self.limit_by.is_empty() { - write!(f, " BY {}", display_separated(&self.limit_by, ", "))?; + + 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(()) } @@ -156,6 +160,8 @@ pub enum SetExpr { Values(Values), Insert(Statement), Update(Statement), + Delete(Statement), + Merge(Statement), Table(Box
), } @@ -173,28 +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::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(()) } } @@ -245,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(()), } } } @@ -275,6 +293,19 @@ impl fmt::Display for Table { } } +/// What did this select look like? +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SelectFlavor { + /// `SELECT *` + Standard, + /// `FROM ... SELECT *` + FromFirst, + /// `FROM *` + FromFirstNoSelect, +} + /// A restricted variant of `SELECT` (without CTEs/`ORDER BY`), which may /// appear either as the only body item of a `Query`, or as an operand /// to a set operation like `UNION`. @@ -292,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 @@ -312,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 @@ -328,97 +364,145 @@ pub struct Select { pub value_table_mode: Option, /// STARTING WITH .. CONNECT BY pub connect_by: Option, + /// Was this a FROM-first query? + pub flavor: SelectFlavor, } impl fmt::Display for Select { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "SELECT")?; + match self.flavor { + SelectFlavor::Standard => { + write!(f, "SELECT")?; + } + SelectFlavor::FromFirst => { + write!(f, "FROM {} SELECT", display_comma_separated(&self.from))?; + } + SelectFlavor::FromFirstNoSelect => { + write!(f, "FROM {}", display_comma_separated(&self.from))?; + } + } 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.from.is_empty() { - write!(f, " FROM {}", display_comma_separated(&self.from))?; + if self.flavor == SelectFlavor::Standard && !self.from.is_empty() { + 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(()) } @@ -524,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(()) } } @@ -576,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}")?; @@ -586,6 +686,20 @@ impl fmt::Display for Cte { } } +/// Represents an expression behind a wildcard expansion in a projection. +/// `SELECT T.* FROM T; +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SelectItemQualifiedWildcardKind { + /// Expression is an object name. + /// e.g. `alias.*` or even `schema.table.*` + ObjectName(ObjectName), + /// Select star on an arbitrary expression. + /// e.g. `STRUCT('foo').*` + Expr(Expr), +} + /// One item of the comma-separated list following `SELECT` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -595,12 +709,24 @@ pub enum SelectItem { UnnamedExpr(Expr), /// An expression, followed by `[ AS ] alias` ExprWithAlias { expr: Expr, alias: Ident }, - /// `alias.*` or even `schema.table.*` - QualifiedWildcard(ObjectName, WildcardAdditionalOptions), + /// An expression, followed by a wildcard expansion. + /// e.g. `alias.*`, `STRUCT('foo').*` + QualifiedWildcard(SelectItemQualifiedWildcardKind, WildcardAdditionalOptions), /// An unqualified `*` Wildcard(WildcardAdditionalOptions), } +impl fmt::Display for SelectItemQualifiedWildcardKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + SelectItemQualifiedWildcardKind::ObjectName(object_name) => { + write!(f, "{object_name}.*") + } + SelectItemQualifiedWildcardKind::Expr(expr) => write!(f, "{expr}.*"), + } + } +} + /// Single aliased identifier /// /// # Syntax @@ -864,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::QualifiedWildcard(prefix, additional_options) => { - write!(f, "{prefix}.*")?; - write!(f, "{additional_options}")?; - Ok(()) + 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) => { + 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) } } } @@ -891,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(()) } @@ -928,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 { @@ -962,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))] @@ -975,6 +1125,81 @@ pub struct TableFunctionArgs { pub settings: 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 TableIndexHintType { + Use, + Ignore, + Force, +} + +impl fmt::Display for TableIndexHintType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + TableIndexHintType::Use => "USE", + TableIndexHintType::Ignore => "IGNORE", + TableIndexHintType::Force => "FORCE", + }) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableIndexType { + Index, + Key, +} + +impl fmt::Display for TableIndexType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + TableIndexType::Index => "INDEX", + TableIndexType::Key => "KEY", + }) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableIndexHintForClause { + Join, + OrderBy, + GroupBy, +} + +impl fmt::Display for TableIndexHintForClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + TableIndexHintForClause::Join => "JOIN", + TableIndexHintForClause::OrderBy => "ORDER BY", + TableIndexHintForClause::GroupBy => "GROUP BY", + }) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TableIndexHints { + pub hint_type: TableIndexHintType, + pub index_type: TableIndexType, + pub for_clause: Option, + pub index_names: Vec, +} + +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, "({})", display_comma_separated(&self.index_names)) + } +} + /// A table name or a parenthesized subquery with an optional alias #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1009,6 +1234,9 @@ pub enum TableFactor { /// Optional table sample modifier /// See: sample: Option, + /// Optional index hints(mysql) + /// See: + index_hints: Vec, }, Derived { lateral: bool, @@ -1110,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, @@ -1119,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. @@ -1151,13 +1381,68 @@ pub enum TableFactor { symbols: Vec, alias: Option, }, + /// The `XMLTABLE` table-valued function. + /// Part of the SQL standard, supported by PostgreSQL, Oracle, and DB2. + /// + /// + /// + /// ```sql + /// SELECT xmltable.* + /// FROM xmldata, + /// XMLTABLE('//ROWS/ROW' + /// PASSING data + /// COLUMNS id int PATH '@id', + /// ordinality FOR ORDINALITY, + /// "COUNTRY_NAME" text, + /// country_id text PATH 'COUNTRY_ID', + /// size_sq_km float PATH 'SIZE[@unit = "sq_km"]', + /// size_other text PATH 'concat(SIZE[@unit!="sq_km"], " ", SIZE[@unit!="sq_km"]/@unit)', + /// premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified' + /// ); + /// ```` + XmlTable { + /// Optional XMLNAMESPACES clause (empty if not present) + namespaces: Vec, + /// The row-generating XPath expression. + row_expression: Expr, + /// The PASSING clause specifying the document expression. + passing: XmlPassingClause, + /// The columns to be extracted from each generated row. + columns: Vec, + /// 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), @@ -1211,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, ")")?; @@ -1304,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(()) } @@ -1403,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(()) } @@ -1529,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, " ")), @@ -1590,10 +1875,11 @@ impl fmt::Display for TableFactor { with_ordinality, json_path, 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))?; @@ -1613,19 +1899,22 @@ 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}")?; } + if !index_hints.is_empty() { + write!(f, " {}", display_separated(index_hints, " "))?; + } if !with_hints.is_empty() { 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(()) } @@ -1637,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}")?; } @@ -1745,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})")?; } @@ -1760,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) @@ -1812,6 +2114,65 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::XmlTable { + row_expression, + passing, + columns, + alias, + namespaces, + } => { + write!(f, "XMLTABLE(")?; + if !namespaces.is_empty() { + write!( + f, + "XMLNAMESPACES({}), ", + display_comma_separated(namespaces) + )?; + } + write!( + f, + "{row_expression}{passing} COLUMNS {columns})", + columns = display_comma_separated(columns) + )?; + if let Some(alias) = alias { + write!(f, " AS {alias}")?; + } + 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(()) + } } } } @@ -1863,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(()) } @@ -1873,13 +2234,19 @@ impl fmt::Display for TableAliasColumnDef { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TableVersion { + /// When the table version is defined using `FOR SYSTEM_TIME AS OF`. + /// For example: `SELECT * FROM tbl FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)` ForSystemTimeAsOf(Expr), + /// When the table version is defined using a function. + /// For example: `SELECT * FROM tbl AT(TIMESTAMP => '2020-08-14 09:30:00')` + Function(Expr), } 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::ForSystemTimeAsOf(e) => write!(f, "FOR SYSTEM_TIME AS OF {e}")?, + TableVersion::Function(func) => write!(f, "{func}")?, } Ok(()) } @@ -1920,92 +2287,108 @@ impl fmt::Display for Join { Suffix(constraint) } if self.global { - write!(f, " GLOBAL")?; + write!(f, "GLOBAL ")?; } match &self.join_operator { - JoinOperator::Inner(constraint) => write!( - f, - " {}JOIN {}{}", + JoinOperator::Join(constraint) => f.write_fmt(format_args!( + "{}JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::LeftOuter(constraint) => write!( - f, - " {}LEFT JOIN {}{}", + )), + JoinOperator::Inner(constraint) => f.write_fmt(format_args!( + "{}INNER JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::RightOuter(constraint) => write!( - f, - " {}RIGHT JOIN {}{}", + )), + JoinOperator::Left(constraint) => f.write_fmt(format_args!( + "{}LEFT JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::FullOuter(constraint) => write!( - f, - " {}FULL JOIN {}{}", + )), + JoinOperator::LeftOuter(constraint) => f.write_fmt(format_args!( + "{}LEFT OUTER JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::CrossJoin => write!(f, " CROSS JOIN {}", self.relation), - JoinOperator::Semi(constraint) => write!( - f, - " {}SEMI JOIN {}{}", + )), + JoinOperator::Right(constraint) => f.write_fmt(format_args!( + "{}RIGHT JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::LeftSemi(constraint) => write!( - f, - " {}LEFT SEMI JOIN {}{}", + )), + JoinOperator::RightOuter(constraint) => f.write_fmt(format_args!( + "{}RIGHT OUTER JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::RightSemi(constraint) => write!( - f, - " {}RIGHT SEMI JOIN {}{}", + )), + JoinOperator::FullOuter(constraint) => f.write_fmt(format_args!( + "{}FULL JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::Anti(constraint) => write!( - f, - " {}ANTI JOIN {}{}", + )), + JoinOperator::CrossJoin(constraint) => f.write_fmt(format_args!( + "CROSS JOIN {}{}", + self.relation, + suffix(constraint) + )), + JoinOperator::Semi(constraint) => f.write_fmt(format_args!( + "{}SEMI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::LeftAnti(constraint) => write!( - f, - " {}LEFT ANTI JOIN {}{}", + )), + JoinOperator::LeftSemi(constraint) => f.write_fmt(format_args!( + "{}LEFT SEMI JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) - ), - JoinOperator::RightAnti(constraint) => write!( - f, - " {}RIGHT ANTI JOIN {}{}", + )), + JoinOperator::RightSemi(constraint) => f.write_fmt(format_args!( + "{}RIGHT SEMI JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + )), + JoinOperator::Anti(constraint) => f.write_fmt(format_args!( + "{}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::LeftAnti(constraint) => f.write_fmt(format_args!( + "{}LEFT ANTI JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + )), + JoinOperator::RightAnti(constraint) => f.write_fmt(format_args!( + "{}RIGHT ANTI JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + )), + 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) => f.write_fmt(format_args!( + "STRAIGHT_JOIN {}{}", self.relation, suffix(constraint) - ), + )), } } } @@ -2014,11 +2397,15 @@ impl fmt::Display for Join { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum JoinOperator { + Join(JoinConstraint), Inner(JoinConstraint), + Left(JoinConstraint), LeftOuter(JoinConstraint), + Right(JoinConstraint), RightOuter(JoinConstraint), FullOuter(JoinConstraint), - CrossJoin, + /// CROSS (constraint is non-standard) + CrossJoin(JoinConstraint), /// SEMI (non-standard) Semi(JoinConstraint), /// LEFT SEMI (non-standard) @@ -2043,6 +2430,10 @@ pub enum JoinOperator { match_condition: Expr, constraint: JoinConstraint, }, + /// STRAIGHT_JOIN (non-standard) + /// + /// See . + StraightJoin(JoinConstraint), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -2050,35 +2441,55 @@ pub enum JoinOperator { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum JoinConstraint { On(Expr), - Using(Vec), + Using(Vec), Natural, None, } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum OrderByKind { + /// ALL syntax of [DuckDB] and [ClickHouse]. + /// + /// [DuckDB]: + /// [ClickHouse]: + All(OrderByOptions), + + /// Expressions + Expressions(Vec), +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct OrderBy { - pub exprs: Vec, + pub kind: OrderByKind, + /// Optional: `INTERPOLATE` /// Supported by [ClickHouse syntax] - /// - /// [ClickHouse syntax]: pub interpolate: Option, } impl fmt::Display for OrderBy { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "ORDER BY")?; - if !self.exprs.is_empty() { - write!(f, " {}", display_comma_separated(&self.exprs))?; + match &self.kind { + OrderByKind::Expressions(exprs) => { + write!(f, " {}", display_comma_separated(exprs))?; + } + OrderByKind::All(all) => { + write!(f, " ALL{all}")?; + } } + if let Some(ref interpolate) = self.interpolate { match &interpolate.exprs { Some(exprs) => write!(f, " INTERPOLATE ({})", display_comma_separated(exprs))?, None => write!(f, " INTERPOLATE")?, } } + Ok(()) } } @@ -2089,30 +2500,27 @@ impl fmt::Display for OrderBy { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct OrderByExpr { pub expr: Expr, - /// Optional `ASC` or `DESC` - pub asc: Option, - /// Optional `NULLS FIRST` or `NULLS LAST` - pub nulls_first: Option, + pub options: OrderByOptions, /// Optional: `WITH FILL` /// Supported by [ClickHouse syntax]: pub with_fill: Option, } +impl From for OrderByExpr { + fn from(ident: Ident) -> Self { + OrderByExpr { + expr: Expr::Identifier(ident), + options: OrderByOptions::default(), + with_fill: None, + } + } +} + impl fmt::Display for OrderByExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.expr)?; - match self.asc { - Some(true) => write!(f, " ASC")?, - Some(false) => write!(f, " DESC")?, - None => (), - } - match self.nulls_first { - Some(true) => write!(f, " NULLS FIRST")?, - Some(false) => write!(f, " NULLS LAST")?, - None => (), - } + write!(f, "{}{}", self.expr, self.options)?; if let Some(ref with_fill) = self.with_fill { - write!(f, " {}", with_fill)? + write!(f, " {with_fill}")? } Ok(()) } @@ -2135,13 +2543,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(()) } @@ -2170,12 +2578,90 @@ 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(()) } } +#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct OrderByOptions { + /// Optional `ASC` or `DESC` + pub asc: Option, + /// Optional `NULLS FIRST` or `NULLS LAST` + pub nulls_first: Option, +} + +impl fmt::Display for OrderByOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.asc { + Some(true) => write!(f, " ASC")?, + Some(false) => write!(f, " DESC")?, + None => (), + } + match self.nulls_first { + Some(true) => write!(f, " NULLS FIRST")?, + Some(false) => write!(f, " NULLS LAST")?, + None => (), + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum LimitClause { + /// Standard SQL syntax + /// + /// `LIMIT [BY ,,...] [OFFSET ]` + LimitOffset { + /// `LIMIT { | ALL }` + limit: Option, + /// `OFFSET [ { ROW | ROWS } ]` + offset: Option, + /// `BY { ,,... } }` + /// + /// [ClickHouse](https://clickhouse.com/docs/sql-reference/statements/select/limit-by) + limit_by: Vec, + }, + /// [MySQL]-specific syntax; the order of expressions is reversed. + /// + /// `LIMIT , ` + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/select.html + OffsetCommaLimit { offset: Expr, limit: Expr }, +} + +impl fmt::Display for LimitClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + LimitClause::LimitOffset { + limit, + limit_by, + offset, + } => { + if let Some(ref limit) = limit { + write!(f, " LIMIT {limit}")?; + } + if let Some(ref offset) = offset { + write!(f, " {offset}")?; + } + if !limit_by.is_empty() { + debug_assert!(limit.is_some()); + write!(f, " BY {}", display_separated(limit_by, ", "))?; + } + Ok(()) + } + LimitClause::OffsetCommaLimit { offset, limit } => { + write!(f, " LIMIT {offset}, {limit}") + } + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2211,6 +2697,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))] @@ -2359,18 +3135,25 @@ pub struct Values { /// Was there an explicit ROWs keyword (MySQL)? /// pub explicit_row: bool, + // MySql supports both VALUES and VALUE keywords. + // + pub value_keyword: bool, pub rows: Vec>, } impl fmt::Display for Values { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "VALUES ")?; + match self.value_keyword { + true => f.write_str("VALUE")?, + false => 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(()) } @@ -2400,13 +3183,18 @@ impl fmt::Display for SelectInto { /// e.g. GROUP BY year WITH ROLLUP WITH TOTALS /// /// [ClickHouse]: -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum GroupByWithModifier { Rollup, Cube, Totals, + /// Hive supports GROUP BY GROUPING SETS syntax. + /// e.g. GROUP BY year , month GROUPING SETS((year,month),(year),(month)) + /// + /// [Hive]: + GroupingSets(Expr), } impl fmt::Display for GroupByWithModifier { @@ -2415,6 +3203,9 @@ impl fmt::Display for GroupByWithModifier { GroupByWithModifier::Rollup => write!(f, "WITH ROLLUP"), GroupByWithModifier::Cube => write!(f, "WITH CUBE"), GroupByWithModifier::Totals => write!(f, "WITH TOTALS"), + GroupByWithModifier::GroupingSets(expr) => { + write!(f, "{expr}") + } } } } @@ -2449,8 +3240,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, " "))?; } @@ -2474,7 +3266,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"), } } @@ -2536,9 +3328,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")?; @@ -2556,7 +3348,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")?; } @@ -2564,7 +3356,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")?; @@ -2591,7 +3383,7 @@ impl fmt::Display for ForXml { ForXml::Raw(root) => { write!(f, "RAW")?; if let Some(root) = root { - write!(f, "('{}')", root)?; + write!(f, "('{root}')")?; } Ok(()) } @@ -2600,7 +3392,7 @@ impl fmt::Display for ForXml { ForXml::Path(root) => { write!(f, "PATH")?; if let Some(root) = root { - write!(f, "('{}')", root)?; + write!(f, "('{root}')")?; } Ok(()) } @@ -2663,7 +3455,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}") } @@ -2729,10 +3521,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(()) } @@ -2754,7 +3546,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"), } @@ -2796,15 +3588,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 { @@ -2812,6 +3608,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"), } } } @@ -2821,10 +3619,140 @@ impl fmt::Display for ValueTableMode { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum UpdateTableFromKind { - /// Update Statment where the 'FROM' clause is before the 'SET' keyword (Supported by Snowflake) + /// Update Statement where the 'FROM' clause is before the 'SET' keyword (Supported by Snowflake) /// For Example: `UPDATE FROM t1 SET t1.name='aaa'` - BeforeSet(TableWithJoins), - /// Update Statment where the 'FROM' clause is after the 'SET' keyword (Which is the standard way) + BeforeSet(Vec), + /// Update Statement where the 'FROM' clause is after the 'SET' keyword (Which is the standard way) /// For Example: `UPDATE SET t1.name='aaa' FROM t1` - AfterSet(TableWithJoins), + AfterSet(Vec), +} + +/// Defines the options for an XmlTable column: Named or ForOrdinality +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum XmlTableColumnOption { + /// A named column with a type, optional path, and default value. + NamedInfo { + /// The type of the column to be extracted. + r#type: DataType, + /// The path to the column to be extracted. If None, defaults to the column name. + path: Option, + /// Default value if path does not match + default: Option, + /// Whether the column is nullable (NULL=true, NOT NULL=false) + nullable: bool, + }, + /// The FOR ORDINALITY marker + ForOrdinality, +} + +/// A single column definition in XMLTABLE +/// +/// ```sql +/// COLUMNS +/// id int PATH '@id', +/// ordinality FOR ORDINALITY, +/// "COUNTRY_NAME" text, +/// country_id text PATH 'COUNTRY_ID', +/// size_sq_km float PATH 'SIZE[@unit = "sq_km"]', +/// size_other text PATH 'concat(SIZE[@unit!="sq_km"], " ", SIZE[@unit!="sq_km"]/@unit)', +/// premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified' +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct XmlTableColumn { + /// The name of the column. + pub name: Ident, + /// Column options: type/path/default or FOR ORDINALITY + pub option: XmlTableColumnOption, +} + +impl fmt::Display for XmlTableColumn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name)?; + match &self.option { + XmlTableColumnOption::NamedInfo { + r#type, + path, + default, + nullable, + } => { + write!(f, " {type}")?; + if let Some(p) = path { + write!(f, " PATH {p}")?; + } + if let Some(d) = default { + write!(f, " DEFAULT {d}")?; + } + if !*nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } + XmlTableColumnOption::ForOrdinality => { + write!(f, " FOR ORDINALITY") + } + } + } +} + +/// Argument passed in the XMLTABLE PASSING clause +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct XmlPassingArgument { + pub expr: Expr, + pub alias: Option, + pub by_value: bool, // True if BY VALUE is specified +} + +impl fmt::Display for XmlPassingArgument { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.by_value { + write!(f, "BY VALUE ")?; + } + write!(f, "{}", self.expr)?; + if let Some(alias) = &self.alias { + write!(f, " AS {alias}")?; + } + Ok(()) + } +} + +/// The PASSING clause for XMLTABLE +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct XmlPassingClause { + pub arguments: Vec, +} + +impl fmt::Display for XmlPassingClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.arguments.is_empty() { + write!(f, " PASSING {}", display_comma_separated(&self.arguments))?; + } + Ok(()) + } +} + +/// Represents a single XML namespace definition in the XMLNAMESPACES clause. +/// +/// `namespace_uri AS namespace_name` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct XmlNamespaceDefinition { + /// The namespace URI (a text expression). + pub uri: Expr, + /// The alias for the namespace (a simple identifier). + pub name: Ident, +} + +impl fmt::Display for XmlNamespaceDefinition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} AS {}", self.uri, self.name) + } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 183bebf8c..719e261cb 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -15,26 +15,33 @@ // specific language governing permissions and limitations // under the License. +use crate::ast::{ + ddl::AlterSchema, query::SelectItemQualifiedWildcardKind, AlterSchemaOperation, AlterTable, + ColumnOptions, CreateView, ExportData, Owner, TypedString, +}; use core::iter; use crate::tokenizer::Span; use super::{ - dcl::SecondaryRoles, AccessExpr, AlterColumnOperation, AlterIndexOperation, - AlterTableOperation, Array, Assignment, AssignmentTarget, CloseCursor, ClusteredIndex, - ColumnDef, ColumnOption, ColumnOptionDef, ConflictTarget, ConnectBy, ConstraintCharacteristics, - CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, - ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, Function, - FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, - GroupByExpr, HavingBound, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, - JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, MatchRecognizePattern, - Measure, NamedWindowDefinition, ObjectName, Offset, OnConflict, OnConflictAction, OnInsert, - OrderBy, OrderByExpr, Partition, PivotValueSource, ProjectionSelect, Query, ReferentialAction, - RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, - SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, - TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, - UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WildcardAdditionalOptions, With, - WithFill, + dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, + AlterIndexOperation, AlterTableOperation, Analyze, Array, Assignment, AssignmentTarget, + AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, + ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, + ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, + CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, + ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, + FunctionArgumentClause, FunctionArgumentList, 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, Update, UpdateTableFromKind, Use, Value, Values, + ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -93,14 +100,13 @@ impl Spanned for Query { with, body, order_by, - limit, - limit_by, - offset, + 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( @@ -108,14 +114,31 @@ impl Spanned for Query { .map(|i| i.span()) .chain(core::iter::once(body.span())) .chain(order_by.as_ref().map(|i| i.span())) - .chain(limit.as_ref().map(|i| i.span())) - .chain(limit_by.iter().map(|i| i.span())) - .chain(offset.as_ref().map(|i| i.span())) + .chain(limit_clause.as_ref().map(|i| i.span())) .chain(fetch.as_ref().map(|i| i.span())), ) } } +impl Spanned for LimitClause { + fn span(&self) -> Span { + match self { + LimitClause::LimitOffset { + limit, + offset, + limit_by, + } => union_spans( + limit + .iter() + .map(|i| i.span()) + .chain(offset.as_ref().map(|i| i.span())) + .chain(limit_by.iter().map(|i| i.span())), + ), + LimitClause::OffsetCommaLimit { offset, limit } => offset.span().union(&limit.span()), + } + } +} + impl Spanned for Offset { fn span(&self) -> Span { let Offset { @@ -190,6 +213,8 @@ impl Spanned for SetExpr { SetExpr::Insert(statement) => statement.span(), SetExpr::Table(_) => Span::empty(), SetExpr::Update(statement) => statement.span(), + SetExpr::Delete(statement) => statement.span(), + SetExpr::Merge(statement) => statement.span(), } } } @@ -198,6 +223,7 @@ impl Spanned for Values { fn span(&self) -> Span { let Values { explicit_row: _, // bool, + value_keyword: _, rows, } = self; @@ -214,6 +240,7 @@ impl Spanned for Values { /// - [Statement::CopyIntoSnowflake] /// - [Statement::CreateSecret] /// - [Statement::CreateRole] +/// - [Statement::AlterType] /// - [Statement::AlterRole] /// - [Statement::AttachDatabase] /// - [Statement::AttachDuckDBDatabase] @@ -227,11 +254,7 @@ impl Spanned for Values { /// - [Statement::Fetch] /// - [Statement::Flush] /// - [Statement::Discard] -/// - [Statement::SetRole] -/// - [Statement::SetVariable] -/// - [Statement::SetTimeZone] -/// - [Statement::SetNames] -/// - [Statement::SetNamesDefault] +/// - [Statement::Set] /// - [Statement::ShowFunctions] /// - [Statement::ShowVariable] /// - [Statement::ShowStatus] @@ -241,7 +264,6 @@ impl Spanned for Values { /// - [Statement::ShowTables] /// - [Statement::ShowCollation] /// - [Statement::StartTransaction] -/// - [Statement::SetTransaction] /// - [Statement::Comment] /// - [Statement::Commit] /// - [Statement::Rollback] @@ -277,39 +299,9 @@ impl Spanned for Values { impl Spanned for Statement { fn span(&self) -> Span { match self { - Statement::Analyze { - table_name, - partitions, - for_columns: _, - columns, - cache_metadata: _, - noscan: _, - compute_statistics: _, - has_table_keyword: _, - } => union_spans( - core::iter::once(table_name.span()) - .chain(partitions.iter().flat_map(|i| i.iter().map(|k| k.span()))) - .chain(columns.iter().map(|i| i.span)), - ), - Statement::Truncate { - table_names, - partitions, - table: _, - only: _, - identity: _, - cascade: _, - on_cluster: _, - } => union_spans( - table_names - .iter() - .map(|i| i.name.span()) - .chain(partitions.iter().flat_map(|i| i.iter().map(|k| k.span()))), - ), - Statement::Msck { - table_name, - repair: _, - partition_action: _, - } => table_name.span(), + Statement::Analyze(analyze) => analyze.span(), + Statement::Truncate(truncate) => truncate.span(), + Statement::Msck(msck) => msck.span(), Statement::Query(query) => query.span(), Statement::Insert(insert) => insert.span(), Statement::Install { extension_name } => extension_name.span, @@ -321,6 +313,10 @@ impl Spanned for Statement { file_format: _, source, } => 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 { source, @@ -332,8 +328,9 @@ impl Spanned for Statement { } => source.span(), Statement::CopyIntoSnowflake { into: _, - from_stage: _, - from_stage_alias: _, + into_columns: _, + from_obj: _, + from_obj_alias: _, stage_params: _, from_transformations: _, files: _, @@ -341,48 +338,18 @@ impl Spanned for Statement { file_format: _, copy_options: _, validation_mode: _, + kind: _, + from_query: _, + partition: _, } => Span::empty(), + Statement::Open(open) => open.span(), Statement::Close { cursor } => match cursor { CloseCursor::All => Span::empty(), CloseCursor::Specific { name } => name.span, }, - Statement::Update { - table, - assignments, - from, - selection, - returning, - or: _, - } => union_spans( - core::iter::once(table.span()) - .chain(assignments.iter().map(|i| i.span())) - .chain(from.iter().map(|i| i.span())) - .chain(selection.iter().map(|i| i.span())) - .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))), - ), + Statement::Update(update) => update.span(), Statement::Delete(delete) => delete.span(), - Statement::CreateView { - or_replace: _, - materialized: _, - name, - columns, - query, - options, - cluster_by, - comment: _, - with_no_schema_binding: _, - if_not_exists: _, - temporary: _, - to, - params: _, - } => union_spans( - core::iter::once(name.span()) - .chain(columns.iter().map(|i| i.span())) - .chain(core::iter::once(query.span())) - .chain(core::iter::once(options.span())) - .chain(cluster_by.iter().map(|i| i.span)) - .chain(to.iter().map(|i| i.span())), - ), + Statement::CreateView(create_view) => create_view.span(), Statement::CreateTable(create_table) => create_table.span(), Statement::CreateVirtualTable { name, @@ -395,20 +362,13 @@ impl Spanned for Statement { .chain(module_args.iter().map(|i| i.span)), ), Statement::CreateIndex(create_index) => create_index.span(), - Statement::CreateRole { .. } => Span::empty(), + Statement::CreateRole(create_role) => create_role.span(), + Statement::CreateExtension(create_extension) => create_extension.span(), + Statement::DropExtension(drop_extension) => drop_extension.span(), Statement::CreateSecret { .. } => Span::empty(), - Statement::AlterTable { - name, - if_exists: _, - only: _, - operations, - location: _, - on_cluster, - } => union_spans( - core::iter::once(name.span()) - .chain(operations.iter().map(|i| i.span())) - .chain(on_cluster.iter().map(|i| i.span)), - ), + Statement::CreateServer { .. } => Span::empty(), + Statement::CreateConnector { .. } => Span::empty(), + Statement::AlterTable(alter_table) => alter_table.span(), Statement::AlterIndex { name, operation } => name.span().union(&operation.span()), Statement::AlterView { name, @@ -422,25 +382,22 @@ impl Spanned for Statement { .chain(with_options.iter().map(|i| i.span())), ), // These statements need to be implemented + Statement::AlterType { .. } => Span::empty(), Statement::AlterRole { .. } => Span::empty(), + Statement::AlterSession { .. } => Span::empty(), Statement::AttachDatabase { .. } => Span::empty(), Statement::AttachDuckDBDatabase { .. } => Span::empty(), Statement::DetachDuckDBDatabase { .. } => Span::empty(), Statement::Drop { .. } => Span::empty(), - Statement::DropFunction { .. } => Span::empty(), + Statement::DropFunction(drop_function) => drop_function.span(), + Statement::DropDomain { .. } => Span::empty(), Statement::DropProcedure { .. } => Span::empty(), Statement::DropSecret { .. } => Span::empty(), Statement::Declare { .. } => Span::empty(), - Statement::CreateExtension { .. } => Span::empty(), - Statement::DropExtension { .. } => Span::empty(), Statement::Fetch { .. } => Span::empty(), Statement::Flush { .. } => Span::empty(), Statement::Discard { .. } => Span::empty(), - Statement::SetRole { .. } => Span::empty(), - Statement::SetVariable { .. } => Span::empty(), - Statement::SetTimeZone { .. } => Span::empty(), - Statement::SetNames { .. } => Span::empty(), - Statement::SetNamesDefault {} => Span::empty(), + Statement::Set(_) => Span::empty(), Statement::ShowFunctions { .. } => Span::empty(), Statement::ShowVariable { .. } => Span::empty(), Statement::ShowStatus { .. } => Span::empty(), @@ -449,15 +406,16 @@ 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::SetTransaction { .. } => Span::empty(), Statement::Comment { .. } => Span::empty(), Statement::Commit { .. } => Span::empty(), Statement::Rollback { .. } => Span::empty(), 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(), @@ -465,6 +423,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(), @@ -486,17 +445,38 @@ impl Spanned for Statement { Statement::OptimizeTable { .. } => Span::empty(), Statement::CreatePolicy { .. } => Span::empty(), Statement::AlterPolicy { .. } => Span::empty(), + Statement::AlterConnector { .. } => Span::empty(), Statement::DropPolicy { .. } => Span::empty(), + Statement::DropConnector { .. } => Span::empty(), Statement::ShowDatabases { .. } => Span::empty(), Statement::ShowSchemas { .. } => Span::empty(), + Statement::ShowObjects { .. } => Span::empty(), Statement::ShowViews { .. } => Span::empty(), Statement::LISTEN { .. } => Span::empty(), Statement::NOTIFY { .. } => Span::empty(), Statement::LoadData { .. } => Span::empty(), Statement::UNLISTEN { .. } => Span::empty(), Statement::RenameTable { .. } => Span::empty(), + Statement::RaisError { .. } => Span::empty(), + Statement::Print { .. } => Span::empty(), + Statement::Return { .. } => Span::empty(), Statement::List(..) | Statement::Remove(..) => Span::empty(), - Statement::SetSessionParam { .. } => 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(), + Statement::AlterUser(..) => Span::empty(), + Statement::Reset(..) => Span::empty(), } } } @@ -528,35 +508,31 @@ impl Spanned for CreateTable { temporary: _, // bool external: _, // bool global: _, // bool + dynamic: _, // bool if_not_exists: _, // bool transient: _, // bool volatile: _, // bool + iceberg: _, // bool, Snowflake specific name, columns, 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 enable_schema_evolution: _, // bool @@ -567,16 +543,26 @@ impl Spanned for CreateTable { with_aggregation_policy: _, // todo, Snowflake specific with_row_access_policy: _, // todo, Snowflake specific with_tags: _, // todo, Snowflake specific + external_volume: _, // todo, Snowflake specific + base_location: _, // todo, Snowflake specific + catalog: _, // todo, Snowflake specific + catalog_sync: _, // 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())), ) } @@ -587,15 +573,10 @@ impl Spanned for ColumnDef { let ColumnDef { name, data_type: _, // enum - collation, options, } = self; - union_spans( - core::iter::once(name.span) - .chain(collation.iter().map(|i| i.span())) - .chain(options.iter().map(|i| i.span())), - ) + union_spans(core::iter::once(name.span).chain(options.iter().map(|i| i.span()))) } } @@ -610,78 +591,12 @@ impl Spanned for ColumnOptionDef { impl Spanned for TableConstraint { fn span(&self) -> Span { match self { - TableConstraint::Unique { - name, - index_name, - index_type_display: _, - index_type: _, - columns, - index_options: _, - characteristics, - nulls_distinct: _, - } => union_spans( - name.iter() - .map(|i| i.span) - .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) - .chain(characteristics.iter().map(|i| i.span())), - ), - TableConstraint::PrimaryKey { - name, - index_name, - index_type: _, - columns, - index_options: _, - characteristics, - } => union_spans( - name.iter() - .map(|i| i.span) - .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) - .chain(characteristics.iter().map(|i| i.span())), - ), - TableConstraint::ForeignKey { - name, - columns, - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => union_spans( - 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)) - .chain(on_delete.iter().map(|i| i.span())) - .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::Index { - display_as_key: _, - name, - index_type: _, - columns, - } => union_spans( - name.iter() - .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), - ), - TableConstraint::FulltextOrSpatial { - fulltext: _, - index_type_display: _, - opt_index_name, - columns, - } => union_spans( - opt_index_name - .iter() - .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), - ), + TableConstraint::Unique(constraint) => constraint.span(), + TableConstraint::PrimaryKey(constraint) => constraint.span(), + TableConstraint::ForeignKey(constraint) => constraint.span(), + TableConstraint::Check(constraint) => constraint.span(), + TableConstraint::Index(constraint) => constraint.span(), + TableConstraint::FulltextOrSpatial(constraint) => constraint.span(), } } } @@ -691,7 +606,7 @@ impl Spanned for CreateIndex { let CreateIndex { name, table_name, - using, + using: _, columns, unique: _, // bool concurrently: _, // bool @@ -700,28 +615,123 @@ impl Spanned for CreateIndex { nulls_distinct: _, // bool with, predicate, + index_options: _, + alter_options, } = self; union_spans( name.iter() .map(|i| i.span()) .chain(core::iter::once(table_name.span())) - .chain(using.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span())) + .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 { + case_token: AttachedToken(start), + match_expr: _, + when_blocks: _, + else_block: _, + end_case_token: AttachedToken(end), + } = self; + + union_spans([start.span, end.span].into_iter()) + } +} + +impl Spanned for IfStatement { + fn span(&self) -> Span { + let IfStatement { + if_block, + elseif_blocks, + else_block, + end_token, + } = self; + + union_spans( + iter::once(if_block.span()) + .chain(elseif_blocks.iter().map(|b| b.span())) + .chain(else_block.as_ref().map(|b| b.span())) + .chain(end_token.as_ref().map(|AttachedToken(t)| t.span)), + ) + } +} + +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 { + ConditionalStatements::Sequence { statements } => { + union_spans(statements.iter().map(|s| s.span())) + } + ConditionalStatements::BeginEnd(bes) => bes.span(), + } + } +} + +impl Spanned for ConditionalStatementBlock { + fn span(&self) -> Span { + let ConditionalStatementBlock { + start_token: AttachedToken(start_token), + condition, + then_token, + conditional_statements, + } = self; + + union_spans( + iter::once(start_token.span) + .chain(condition.as_ref().map(|c| c.span())) + .chain(then_token.as_ref().map(|AttachedToken(t)| t.span)) + .chain(iter::once(conditional_statements.span())), ) } } +impl Spanned for RaiseStatement { + fn span(&self) -> Span { + let RaiseStatement { value } = self; + + union_spans(value.iter().map(|value| value.span())) + } +} + +impl Spanned for RaiseStatementValue { + fn span(&self) -> Span { + match self { + RaiseStatementValue::UsingMessage(expr) => expr.span(), + RaiseStatementValue::Expr(expr) => expr.span(), + } + } +} + /// # partial span /// /// Missing spans: /// - [ColumnOption::Null] /// - [ColumnOption::NotNull] /// - [ColumnOption::Comment] -/// - [ColumnOption::Unique]¨ +/// - [ColumnOption::PrimaryKey] +/// - [ColumnOption::Unique] /// - [ColumnOption::DialectSpecific] /// - [ColumnOption::Generated] impl Spanned for ColumnOption { @@ -733,23 +743,13 @@ impl Spanned for ColumnOption { ColumnOption::Materialized(expr) => expr.span(), ColumnOption::Ephemeral(expr) => expr.as_ref().map_or(Span::empty(), |e| e.span()), ColumnOption::Alias(expr) => expr.span(), - ColumnOption::Unique { .. } => Span::empty(), - ColumnOption::ForeignKey { - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => union_spans( - core::iter::once(foreign_table.span()) - .chain(referred_columns.iter().map(|i| i.span)) - .chain(on_delete.iter().map(|i| i.span())) - .chain(on_update.iter().map(|i| i.span())) - .chain(characteristics.iter().map(|i| i.span())), - ), - ColumnOption::Check(expr) => expr.span(), + ColumnOption::PrimaryKey(constraint) => constraint.span(), + ColumnOption::Unique(constraint) => constraint.span(), + ColumnOption::Check(constraint) => constraint.span(), + ColumnOption::ForeignKey(constraint) => constraint.span(), ColumnOption::DialectSpecific(_) => Span::empty(), ColumnOption::CharacterSet(object_name) => object_name.span(), + ColumnOption::Collation(object_name) => object_name.span(), ColumnOption::Comment(_) => Span::empty(), ColumnOption::OnUpdate(expr) => expr.span(), ColumnOption::Generated { .. } => Span::empty(), @@ -758,6 +758,8 @@ impl Spanned for ColumnOption { ColumnOption::OnConflict(..) => Span::empty(), ColumnOption::Policy(..) => Span::empty(), ColumnOption::Tags(..) => Span::empty(), + ColumnOption::Srid(..) => Span::empty(), + ColumnOption::Invisible => Span::empty(), } } } @@ -782,6 +784,20 @@ impl Spanned for ConstraintCharacteristics { } } +impl Spanned for Analyze { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.table_name.span()) + .chain( + self.partitions + .iter() + .flat_map(|i| i.iter().map(|k| k.span())), + ) + .chain(self.columns.iter().map(|i| i.span)), + ) + } +} + /// # partial span /// /// Missing spans: @@ -799,6 +815,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(), } @@ -849,6 +866,29 @@ impl Spanned for Delete { } } +impl Spanned for Update { + fn span(&self) -> Span { + let Update { + table, + assignments, + from, + selection, + returning, + or: _, + limit, + } = self; + + union_spans( + core::iter::once(table.span()) + .chain(assignments.iter().map(|i| i.span())) + .chain(from.iter().map(|i| i.span())) + .chain(selection.iter().map(|i| i.span())) + .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))) + .chain(limit.iter().map(|i| i.span())), + ) + } +} + impl Spanned for FromTable { fn span(&self) -> Span { match self { @@ -866,10 +906,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())) } } @@ -886,6 +929,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)), } } } @@ -922,7 +973,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())), } } } @@ -934,7 +989,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: _, @@ -966,10 +1024,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 { @@ -984,7 +1043,9 @@ impl Spanned for AlterTableOperation { } => partition .span() .union_opt(&with_name.as_ref().map(|n| n.span)), - AlterTableOperation::DropPrimaryKey => Span::empty(), + 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, @@ -1048,6 +1109,17 @@ impl Spanned for AlterTableOperation { AlterTableOperation::DropClusteringKey => Span::empty(), AlterTableOperation::SuspendRecluster => Span::empty(), AlterTableOperation::ResumeRecluster => Span::empty(), + AlterTableOperation::Refresh => Span::empty(), + AlterTableOperation::Suspend => Span::empty(), + AlterTableOperation::Resume => Span::empty(), + 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())) + } } } } @@ -1081,16 +1153,21 @@ impl Spanned for ProjectionSelect { } } +/// # partial span +/// +/// Missing spans: +/// - [OrderByKind::All] impl Spanned for OrderBy { fn span(&self) -> Span { - let OrderBy { exprs, interpolate } = self; - - union_spans( - exprs - .iter() - .map(|i| i.span()) - .chain(interpolate.iter().map(|i| i.span())), - ) + match &self.kind { + OrderByKind::All(_) => Span::empty(), + OrderByKind::Expressions(exprs) => union_spans( + exprs + .iter() + .map(|i| i.span()) + .chain(self.interpolate.iter().map(|i| i.span())), + ), + } } } @@ -1258,7 +1335,6 @@ impl Spanned for AssignmentTarget { /// f.e. `IS NULL ` reports as `::span`. /// /// Missing spans: -/// - [Expr::TypedString] /// - [Expr::MatchAgainst] # MySQL specific /// - [Expr::RLike] # MySQL specific /// - [Expr::Struct] # BigQuery specific @@ -1271,7 +1347,6 @@ impl Spanned for Expr { match self { Expr::Identifier(ident) => ident.span, Expr::CompoundIdentifier(vec) => union_spans(vec.iter().map(|i| i.span)), - Expr::CompositeAccess { expr, key } => expr.span().union(&key.span), Expr::CompoundFieldAccess { root, access_chain } => { union_spans(iter::once(root.span()).chain(access_chain.iter().map(|i| i.span()))) } @@ -1324,6 +1399,12 @@ impl Spanned for Expr { escape_char: _, any: _, } => expr.span().union(&pattern.span()), + Expr::RLike { .. } => Span::empty(), + Expr::IsNormalized { + expr, + form: _, + negated: _, + } => expr.span(), Expr::SimilarTo { negated: _, expr, @@ -1345,10 +1426,10 @@ impl Spanned for Expr { .union_opt(&overlay_for.as_ref().map(|i| i.span())), Expr::Collate { expr, collation } => expr .span() - .union(&union_spans(collation.0.iter().map(|i| i.span))), + .union(&union_spans(collation.0.iter().map(|i| i.span()))), Expr::Nested(expr) => expr.span(), Expr::Value(value) => value.span(), - Expr::TypedString { .. } => Span::empty(), + 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()))) @@ -1359,7 +1440,6 @@ impl Spanned for Expr { Expr::Array(array) => array.span(), Expr::MatchAgainst { .. } => Span::empty(), Expr::JsonAccess { value, path } => value.span().union(&path.span()), - Expr::RLike { .. } => Span::empty(), Expr::AnyOp { left, compare_op: _, @@ -1404,6 +1484,7 @@ impl Spanned for Expr { substring_from, substring_for, special: _, + shorthand: _, } => union_spans( core::iter::once(expr.span()) .chain(substring_from.as_ref().map(|i| i.span())) @@ -1423,20 +1504,26 @@ impl Spanned for Expr { .map(|items| union_spans(items.iter().map(|i| i.span()))), ), ), - Expr::IntroducedString { value, .. } => value.span(), + Expr::Prefixed { value, .. } => value.span(), Expr::Case { + case_token, + end_token, operand, conditions, - results, else_result, } => union_spans( - operand - .as_ref() - .map(|i| i.span()) - .into_iter() - .chain(conditions.iter().map(|i| i.span())) - .chain(results.iter().map(|i| i.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(), @@ -1450,13 +1537,13 @@ impl Spanned for Expr { object_name .0 .iter() - .map(|i| i.span) + .map(|i| i.span()) .chain(iter::once(token.0.span)), ), Expr::OuterJoin(expr) => expr.span(), Expr::Prior(expr) => expr.span(), Expr::Lambda(_) => Span::empty(), - Expr::Method(_) => Span::empty(), + Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()), } } } @@ -1495,7 +1582,19 @@ impl Spanned for ObjectName { fn span(&self) -> Span { let ObjectName(segments) = self; - union_spans(segments.iter().map(|i| i.span)) + union_spans(segments.iter().map(|i| i.span())) + } +} + +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()))), + } } } @@ -1526,7 +1625,7 @@ impl Spanned for Function { union_spans( name.0 .iter() - .map(|i| i.span) + .map(|i| i.span()) .chain(iter::once(args.span())) .chain(iter::once(parameters.span())) .chain(filter.iter().map(|i| i.span())) @@ -1575,6 +1674,7 @@ impl Spanned for FunctionArgumentClause { FunctionArgumentClause::Having(HavingBound(_kind, expr)) => expr.span(), FunctionArgumentClause::Separator(value) => value.span(), FunctionArgumentClause::JsonNullClause(_) => Span::empty(), + FunctionArgumentClause::JsonReturningClause(_) => Span::empty(), } } } @@ -1603,16 +1703,23 @@ impl Spanned for JsonPathElem { } } +impl Spanned for SelectItemQualifiedWildcardKind { + fn span(&self) -> Span { + match self { + SelectItemQualifiedWildcardKind::ObjectName(object_name) => object_name.span(), + SelectItemQualifiedWildcardKind::Expr(expr) => expr.span(), + } + } +} + impl Spanned for SelectItem { fn span(&self) -> Span { match self { SelectItem::UnnamedExpr(expr) => expr.span(), SelectItem::ExprWithAlias { expr, alias } => expr.span().union(&alias.span), - SelectItem::QualifiedWildcard(object_name, wildcard_additional_options) => union_spans( - object_name - .0 - .iter() - .map(|i| i.span) + SelectItem::QualifiedWildcard(kind, wildcard_additional_options) => union_spans( + [kind.span()] + .into_iter() .chain(iter::once(wildcard_additional_options.span())), ), SelectItem::Wildcard(wildcard_additional_options) => wildcard_additional_options.span(), @@ -1719,10 +1826,11 @@ impl Spanned for TableFactor { partitions: _, json_path: _, sample: _, + index_hints: _, } => union_spans( name.0 .iter() - .map(|i| i.span) + .map(|i| i.span()) .chain(alias.as_ref().map(|alias| { union_spans( iter::once(alias.name.span) @@ -1767,11 +1875,12 @@ impl Spanned for TableFactor { } => union_spans( name.0 .iter() - .map(|i| i.span) + .map(|i| i.span()) .chain(args.iter().map(|i| i.span())) .chain(alias.as_ref().map(|alias| alias.span())), ), TableFactor::JsonTable { .. } => Span::empty(), + TableFactor::XmlTable { .. } => Span::empty(), TableFactor::Pivot { table, aggregate_functions, @@ -1782,7 +1891,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())), @@ -1790,14 +1899,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 { @@ -1819,6 +1929,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(), } } @@ -1869,8 +1996,7 @@ impl Spanned for OrderByExpr { fn span(&self) -> Span { let OrderByExpr { expr, - asc: _, // bool - nulls_first: _, // bool + options: _, with_fill, } = self; @@ -1918,7 +2044,7 @@ impl Spanned for FunctionArgExpr { match self { FunctionArgExpr::Expr(expr) => expr.span(), FunctionArgExpr::QualifiedWildcard(object_name) => { - union_spans(object_name.0.iter().map(|i| i.span)) + union_spans(object_name.0.iter().map(|i| i.span())) } FunctionArgExpr::Wildcard => Span::empty(), } @@ -1941,10 +2067,13 @@ impl Spanned for TableAliasColumnDef { } } -/// # missing span -/// -/// The span of a `Value` is currently not implemented, as doing so -/// requires a breaking changes, which may be done in a future release. +impl Spanned for ValueWithSpan { + fn span(&self) -> Span { + self.span + } +} + +/// The span is stored in the `ValueWrapper` struct impl Spanned for Value { fn span(&self) -> Span { Span::empty() // # todo: Value needs to store spans before this is possible @@ -1972,11 +2101,14 @@ impl Spanned for Join { impl Spanned for JoinOperator { fn span(&self) -> Span { match self { + JoinOperator::Join(join_constraint) => join_constraint.span(), JoinOperator::Inner(join_constraint) => join_constraint.span(), + JoinOperator::Left(join_constraint) => join_constraint.span(), JoinOperator::LeftOuter(join_constraint) => join_constraint.span(), + JoinOperator::Right(join_constraint) => join_constraint.span(), JoinOperator::RightOuter(join_constraint) => join_constraint.span(), JoinOperator::FullOuter(join_constraint) => join_constraint.span(), - JoinOperator::CrossJoin => Span::empty(), + JoinOperator::CrossJoin(join_constraint) => join_constraint.span(), JoinOperator::LeftSemi(join_constraint) => join_constraint.span(), JoinOperator::RightSemi(join_constraint) => join_constraint.span(), JoinOperator::LeftAnti(join_constraint) => join_constraint.span(), @@ -1989,6 +2121,7 @@ impl Spanned for JoinOperator { } => match_condition.span().union(&constraint.span()), JoinOperator::Anti(join_constraint) => join_constraint.span(), JoinOperator::Semi(join_constraint) => join_constraint.span(), + JoinOperator::StraightJoin(join_constraint) => join_constraint.span(), } } } @@ -2002,7 +2135,7 @@ impl Spanned for JoinConstraint { fn span(&self) -> Span { match self { JoinConstraint::On(expr) => expr.span(), - JoinConstraint::Using(vec) => union_spans(vec.iter().map(|i| i.span)), + JoinConstraint::Using(vec) => union_spans(vec.iter().map(|i| i.span())), JoinConstraint::Natural => Span::empty(), JoinConstraint::None => Span::empty(), } @@ -2024,6 +2157,7 @@ impl Spanned for Select { distinct: _, // todo top: _, // todo, mysql specific projection, + exclude: _, into, from, lateral_views, @@ -2040,6 +2174,7 @@ impl Spanned for Select { value_table_mode: _, // todo, BigQuery specific connect_by, top_before_distinct: _, + flavor: _, } = self; union_spans( @@ -2118,10 +2253,11 @@ impl Spanned for SelectInto { impl Spanned for UpdateTableFromKind { fn span(&self) -> Span { - match self { - UpdateTableFromKind::BeforeSet(from) => from.span(), - UpdateTableFromKind::AfterSet(from) => from.span(), - } + let from = match self { + UpdateTableFromKind::BeforeSet(from) => from, + UpdateTableFromKind::AfterSet(from) => from, + }; + union_spans(from.iter().map(|t| t.span())) } } @@ -2129,13 +2265,91 @@ impl Spanned for TableObject { fn span(&self) -> Span { match self { TableObject::TableName(ObjectName(segments)) => { - union_spans(segments.iter().map(|i| i.span)) + union_spans(segments.iter().map(|i| i.span())) } TableObject::TableFunction(func) => func.span(), } } } +impl Spanned for BeginEndStatements { + fn span(&self) -> Span { + let BeginEndStatements { + begin_token, + statements, + end_token, + } = self; + union_spans( + core::iter::once(begin_token.0.span) + .chain(statements.iter().map(|i| i.span())) + .chain(core::iter::once(end_token.0.span)), + ) + } +} + +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())) + } + AlterSchemaOperation::Rename { name } => name.span(), + AlterSchemaOperation::OwnerTo { owner } => { + if let Owner::Ident(ident) = owner { + ident.span + } else { + Span::empty() + } + } + } + } +} + +impl Spanned for AlterSchema { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()).chain(self.operations.iter().map(|i| i.span())), + ) + } +} + +impl Spanned for CreateView { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()) + .chain(self.columns.iter().map(|i| i.span())) + .chain(core::iter::once(self.query.span())) + .chain(core::iter::once(self.options.span())) + .chain(self.cluster_by.iter().map(|i| i.span)) + .chain(self.to.iter().map(|i| i.span())), + ) + } +} + +impl Spanned for AlterTable { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()) + .chain(self.operations.iter().map(|i| i.span())) + .chain(self.on_cluster.iter().map(|i| i.span)) + .chain(core::iter::once(self.end_token.0.span)), + ) + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; @@ -2275,4 +2489,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/table_constraints.rs b/src/ast/table_constraints.rs new file mode 100644 index 000000000..ddf0c1253 --- /dev/null +++ b/src/ast/table_constraints.rs @@ -0,0 +1,520 @@ +// 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. + +//! SQL Abstract Syntax Tree (AST) types for table constraints + +use crate::ast::{ + display_comma_separated, display_separated, ConstraintCharacteristics, + ConstraintReferenceMatchKind, Expr, Ident, IndexColumn, IndexOption, IndexType, + KeyOrIndexDisplay, NullsDistinctOption, ObjectName, ReferentialAction, +}; +use crate::tokenizer::Span; +use core::fmt; + +#[cfg(not(feature = "std"))] +use alloc::{boxed::Box, vec::Vec}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +/// A table-level constraint, specified in a `CREATE TABLE` or an +/// `ALTER TABLE ADD ` 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 TableConstraint { + /// MySQL [definition][1] for `UNIQUE` constraints statements:\ + /// * `[CONSTRAINT []] UNIQUE [] [index_type] () ` + /// + /// where: + /// * [index_type][2] is `USING {BTREE | HASH}` + /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` + /// * [index_type_display][4] is `[INDEX | KEY]` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html + /// [2]: IndexType + /// [3]: IndexOption + /// [4]: KeyOrIndexDisplay + Unique(UniqueConstraint), + /// MySQL [definition][1] for `PRIMARY KEY` constraints statements:\ + /// * `[CONSTRAINT []] PRIMARY KEY [index_name] [index_type] () ` + /// + /// Actually the specification have no `[index_name]` but the next query will complete successfully: + /// ```sql + /// CREATE TABLE unspec_table ( + /// xid INT NOT NULL, + /// CONSTRAINT p_name PRIMARY KEY index_name USING BTREE (xid) + /// ); + /// ``` + /// + /// where: + /// * [index_type][2] is `USING {BTREE | HASH}` + /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html + /// [2]: IndexType + /// [3]: IndexOption + PrimaryKey(PrimaryKeyConstraint), + /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () + /// REFERENCES () + /// { [ON DELETE ] [ON UPDATE ] | + /// [ON UPDATE ] [ON DELETE ] + /// }`). + ForeignKey(ForeignKeyConstraint), + /// `[ CONSTRAINT ] CHECK () [[NOT] ENFORCED]` + Check(CheckConstraint), + /// 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. + /// + /// `{INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option]...` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html + Index(IndexConstraint), + /// 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. + /// + /// Supported syntax: + /// + /// ```markdown + /// {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name] (key_part,...) + /// + /// key_part: col_name + /// ``` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html + /// [2]: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html + FulltextOrSpatial(FullTextOrSpatialConstraint), +} + +impl From for TableConstraint { + fn from(constraint: UniqueConstraint) -> Self { + TableConstraint::Unique(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: PrimaryKeyConstraint) -> Self { + TableConstraint::PrimaryKey(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: ForeignKeyConstraint) -> Self { + TableConstraint::ForeignKey(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: CheckConstraint) -> Self { + TableConstraint::Check(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: IndexConstraint) -> Self { + TableConstraint::Index(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: FullTextOrSpatialConstraint) -> Self { + TableConstraint::FulltextOrSpatial(constraint) + } +} + +impl fmt::Display for TableConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TableConstraint::Unique(constraint) => constraint.fmt(f), + TableConstraint::PrimaryKey(constraint) => constraint.fmt(f), + TableConstraint::ForeignKey(constraint) => constraint.fmt(f), + TableConstraint::Check(constraint) => constraint.fmt(f), + TableConstraint::Index(constraint) => constraint.fmt(f), + TableConstraint::FulltextOrSpatial(constraint) => constraint.fmt(f), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CheckConstraint { + pub name: Option, + pub expr: Box, + /// MySQL-specific syntax + /// + pub enforced: Option, +} + +impl fmt::Display for CheckConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::display_constraint_name; + write!( + f, + "{}CHECK ({})", + display_constraint_name(&self.name), + self.expr + )?; + if let Some(b) = self.enforced { + write!(f, " {}", if b { "ENFORCED" } else { "NOT ENFORCED" }) + } else { + Ok(()) + } + } +} + +impl crate::ast::Spanned for CheckConstraint { + fn span(&self) -> Span { + self.expr + .span() + .union_opt(&self.name.as_ref().map(|i| i.span)) + } +} + +/// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () +/// REFERENCES () [ MATCH { FULL | PARTIAL | SIMPLE } ] +/// { [ON DELETE ] [ON UPDATE ] | +/// [ON UPDATE ] [ON DELETE ] +/// }`). +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ForeignKeyConstraint { + pub name: Option, + /// MySQL-specific field + /// + pub index_name: Option, + pub columns: Vec, + pub foreign_table: ObjectName, + pub referred_columns: Vec, + pub on_delete: Option, + pub on_update: Option, + pub match_kind: Option, + pub characteristics: Option, +} + +impl fmt::Display for ForeignKeyConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::{display_constraint_name, display_option_spaced}; + write!( + f, + "{}FOREIGN KEY{} ({}) REFERENCES {}", + display_constraint_name(&self.name), + display_option_spaced(&self.index_name), + display_comma_separated(&self.columns), + self.foreign_table, + )?; + if !self.referred_columns.is_empty() { + write!(f, "({})", display_comma_separated(&self.referred_columns))?; + } + if let Some(match_kind) = &self.match_kind { + write!(f, " {match_kind}")?; + } + if let Some(action) = &self.on_delete { + write!(f, " ON DELETE {action}")?; + } + if let Some(action) = &self.on_update { + write!(f, " ON UPDATE {action}")?; + } + if let Some(characteristics) = &self.characteristics { + write!(f, " {characteristics}")?; + } + Ok(()) + } +} + +impl crate::ast::Spanned for ForeignKeyConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.name + .iter() + .map(|i| i.span) + .chain(self.index_name.iter().map(|i| i.span)) + .chain(self.columns.iter().map(|i| i.span)) + .chain(core::iter::once(self.foreign_table.span())) + .chain(self.referred_columns.iter().map(|i| i.span)) + .chain(self.on_delete.iter().map(|i| i.span())) + .chain(self.on_update.iter().map(|i| i.span())) + .chain(self.characteristics.iter().map(|i| i.span())), + ) + } +} + +/// 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. +/// +/// Supported syntax: +/// +/// ```markdown +/// {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name] (key_part,...) +/// +/// key_part: col_name +/// ``` +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html +/// [2]: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.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 FullTextOrSpatialConstraint { + /// Whether this is a `FULLTEXT` (true) or `SPATIAL` (false) definition. + pub fulltext: bool, + /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. + pub index_type_display: KeyOrIndexDisplay, + /// Optional index name. + pub opt_index_name: Option, + /// Referred column identifier list. + pub columns: Vec, +} + +impl fmt::Display for FullTextOrSpatialConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.fulltext { + write!(f, "FULLTEXT")?; + } else { + write!(f, "SPATIAL")?; + } + + write!(f, "{:>}", self.index_type_display)?; + + if let Some(name) = &self.opt_index_name { + write!(f, " {name}")?; + } + + write!(f, " ({})", display_comma_separated(&self.columns))?; + + Ok(()) + } +} + +impl crate::ast::Spanned for FullTextOrSpatialConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.opt_index_name + .iter() + .map(|i| i.span) + .chain(self.columns.iter().map(|i| i.span())), + ) + } +} + +/// 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. +/// +/// `{INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option]...` +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-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 struct IndexConstraint { + /// Whether this index starts with KEY (true) or INDEX (false), to maintain the same syntax. + pub display_as_key: bool, + /// Index name. + pub name: Option, + /// Optional [index type][1]. + /// + /// [1]: IndexType + pub index_type: Option, + /// Referred column identifier list. + pub columns: Vec, + /// Optional index options such as `USING`; see [`IndexOption`]. + pub index_options: Vec, +} + +impl fmt::Display for IndexConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", if self.display_as_key { "KEY" } else { "INDEX" })?; + if let Some(name) = &self.name { + write!(f, " {name}")?; + } + if let Some(index_type) = &self.index_type { + write!(f, " USING {index_type}")?; + } + write!(f, " ({})", display_comma_separated(&self.columns))?; + if !self.index_options.is_empty() { + write!(f, " {}", display_comma_separated(&self.index_options))?; + } + Ok(()) + } +} + +impl crate::ast::Spanned for IndexConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.name + .iter() + .map(|i| i.span) + .chain(self.columns.iter().map(|i| i.span())), + ) + } +} + +/// MySQL [definition][1] for `PRIMARY KEY` constraints statements: +/// * `[CONSTRAINT []] PRIMARY KEY [index_name] [index_type] () ` +/// +/// Actually the specification have no `[index_name]` but the next query will complete successfully: +/// ```sql +/// CREATE TABLE unspec_table ( +/// xid INT NOT NULL, +/// CONSTRAINT p_name PRIMARY KEY index_name USING BTREE (xid) +/// ); +/// ``` +/// +/// where: +/// * [index_type][2] is `USING {BTREE | HASH}` +/// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` +/// +/// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html +/// [2]: IndexType +/// [3]: IndexOption +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct PrimaryKeyConstraint { + /// Constraint name. + /// + /// Can be not the same as `index_name` + pub name: Option, + /// Index name + pub index_name: Option, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + pub index_type: Option, + /// Identifiers of the columns that form the primary key. + pub columns: Vec, + pub index_options: Vec, + pub characteristics: Option, +} + +impl fmt::Display for PrimaryKeyConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::{display_constraint_name, display_option, display_option_spaced}; + write!( + f, + "{}PRIMARY KEY{}{} ({})", + display_constraint_name(&self.name), + display_option_spaced(&self.index_name), + display_option(" USING ", "", &self.index_type), + display_comma_separated(&self.columns), + )?; + + if !self.index_options.is_empty() { + write!(f, " {}", display_separated(&self.index_options, " "))?; + } + + write!(f, "{}", display_option_spaced(&self.characteristics))?; + Ok(()) + } +} + +impl crate::ast::Spanned for PrimaryKeyConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.name + .iter() + .map(|i| i.span) + .chain(self.index_name.iter().map(|i| i.span)) + .chain(self.columns.iter().map(|i| i.span())) + .chain(self.characteristics.iter().map(|i| i.span())), + ) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct UniqueConstraint { + /// Constraint name. + /// + /// Can be not the same as `index_name` + pub name: Option, + /// Index name + pub index_name: Option, + /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. + pub index_type_display: KeyOrIndexDisplay, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + pub index_type: Option, + /// Identifiers of the columns that are unique. + pub columns: Vec, + pub index_options: Vec, + pub characteristics: Option, + /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` + pub nulls_distinct: NullsDistinctOption, +} + +impl fmt::Display for UniqueConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::{display_constraint_name, display_option, display_option_spaced}; + write!( + f, + "{}UNIQUE{}{:>}{}{} ({})", + display_constraint_name(&self.name), + self.nulls_distinct, + self.index_type_display, + display_option_spaced(&self.index_name), + display_option(" USING ", "", &self.index_type), + display_comma_separated(&self.columns), + )?; + + if !self.index_options.is_empty() { + write!(f, " {}", display_separated(&self.index_options, " "))?; + } + + write!(f, "{}", display_option_spaced(&self.characteristics))?; + Ok(()) + } +} + +impl crate::ast::Spanned for UniqueConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.name + .iter() + .map(|i| i.span) + .chain(self.index_name.iter().map(|i| i.span)) + .chain(self.columns.iter().map(|i| i.span())) + .chain(self.characteristics.iter().map(|i| i.span())), + ) + } +} 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 45cc06a07..fdfa6a674 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -26,14 +26,96 @@ use bigdecimal::BigDecimal; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::ast::Ident; +use crate::{ast::Ident, tokenizer::Span}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +/// Wraps a primitive SQL [`Value`] with its [`Span`] location +/// +/// # Example: create a `ValueWithSpan` from a `Value` +/// ``` +/// # use sqlparser::ast::{Value, ValueWithSpan}; +/// # use sqlparser::tokenizer::{Location, Span}; +/// let value = Value::SingleQuotedString(String::from("endpoint")); +/// // from line 1, column 1 to line 1, column 7 +/// let span = Span::new(Location::new(1, 1), Location::new(1, 7)); +/// let value_with_span = value.with_span(span); +/// ``` +/// +/// # Example: create a `ValueWithSpan` from a `Value` with an empty span +/// +/// You can call [`Value::with_empty_span`] to create a `ValueWithSpan` with an empty span +/// ``` +/// # use sqlparser::ast::{Value, ValueWithSpan}; +/// # use sqlparser::tokenizer::{Location, Span}; +/// let value = Value::SingleQuotedString(String::from("endpoint")); +/// let value_with_span = value.with_empty_span(); +/// assert_eq!(value_with_span.span, Span::empty()); +/// ``` +/// +/// You can also use the [`From`] trait to convert `ValueWithSpan` to/from `Value`s +/// ``` +/// # use sqlparser::ast::{Value, ValueWithSpan}; +/// # use sqlparser::tokenizer::{Location, Span}; +/// let value = Value::SingleQuotedString(String::from("endpoint")); +/// // converting `Value` to `ValueWithSpan` results in an empty span +/// let value_with_span: ValueWithSpan = value.into(); +/// assert_eq!(value_with_span.span, Span::empty()); +/// // convert back to `Value` +/// let value: Value = value_with_span.into(); +/// ``` +#[derive(Debug, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ValueWithSpan { + pub value: Value, + pub span: Span, +} + +impl PartialEq for ValueWithSpan { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl Ord for ValueWithSpan { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.value.cmp(&other.value) + } +} + +impl PartialOrd for ValueWithSpan { + fn partial_cmp(&self, other: &Self) -> Option { + Some(Ord::cmp(self, other)) + } +} + +impl core::hash::Hash for ValueWithSpan { + fn hash(&self, state: &mut H) { + self.value.hash(state); + } +} + +impl From for ValueWithSpan { + fn from(value: Value) -> Self { + value.with_empty_span() + } +} + +impl From for Value { + fn from(value: ValueWithSpan) -> Self { + value.value + } +} + /// Primitive SQL values such as number and string #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr( + feature = "visitor", + derive(Visit, VisitMut), + visit(with = "visit_value") +)] pub enum Value { /// Numeric literal #[cfg(not(feature = "bigdecimal"))] @@ -97,6 +179,53 @@ pub enum Value { Placeholder(String), } +impl ValueWithSpan { + /// If the underlying literal is a string, regardless of quote style, returns the associated string value + pub fn into_string(self) -> Option { + self.value.into_string() + } +} + +impl Value { + /// If the underlying literal is a string, regardless of quote style, returns the associated string value + pub fn into_string(self) -> Option { + match self { + Value::SingleQuotedString(s) + | Value::DoubleQuotedString(s) + | Value::TripleSingleQuotedString(s) + | Value::TripleDoubleQuotedString(s) + | Value::SingleQuotedByteStringLiteral(s) + | Value::DoubleQuotedByteStringLiteral(s) + | Value::TripleSingleQuotedByteStringLiteral(s) + | Value::TripleDoubleQuotedByteStringLiteral(s) + | Value::SingleQuotedRawStringLiteral(s) + | Value::DoubleQuotedRawStringLiteral(s) + | Value::TripleSingleQuotedRawStringLiteral(s) + | Value::TripleDoubleQuotedRawStringLiteral(s) + | Value::EscapedStringLiteral(s) + | Value::UnicodeStringLiteral(s) + | Value::NationalStringLiteral(s) + | Value::HexStringLiteral(s) => Some(s), + Value::DollarQuotedString(s) => Some(s.value), + _ => None, + } + } + + pub fn with_span(self, span: Span) -> ValueWithSpan { + ValueWithSpan { value: self, span } + } + + pub fn with_empty_span(self) -> ValueWithSpan { + self.with_span(Span::empty()) + } +} + +impl fmt::Display for ValueWithSpan { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.value) + } +} + impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -270,6 +399,35 @@ impl fmt::Display for DateTimeField { } } +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// The Unicode Standard defines four normalization forms, which are intended to eliminate +/// certain distinctions between visually or functionally identical characters. +/// +/// See [Unicode Normalization Forms](https://unicode.org/reports/tr15/) for details. +pub enum NormalizationForm { + /// Canonical Decomposition, followed by Canonical Composition. + NFC, + /// Canonical Decomposition. + NFD, + /// Compatibility Decomposition, followed by Canonical Composition. + NFKC, + /// Compatibility Decomposition. + NFKD, +} + +impl fmt::Display for NormalizationForm { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NormalizationForm::NFC => write!(f, "NFC"), + NormalizationForm::NFD => write!(f, "NFD"), + NormalizationForm::NFKC => write!(f, "NFKC"), + NormalizationForm::NFKD => write!(f, "NFKD"), + } + } +} + pub struct EscapeQuotedString<'a> { string: &'a str, quote: char, @@ -297,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(()) } } @@ -384,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 c824ad2f3..328f925f7 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -17,7 +17,7 @@ //! Recursive visitors for ast Nodes. See [`Visitor`] for more details. -use crate::ast::{Expr, ObjectName, Query, Statement, TableFactor}; +use crate::ast::{Expr, ObjectName, Query, Statement, TableFactor, Value}; use core::ops::ControlFlow; /// A type that can be visited by a [`Visitor`]. See [`Visitor`] for @@ -182,6 +182,10 @@ visit_noop!(bigdecimal::BigDecimal); /// ``` pub trait Visitor { /// Type returned when the recursion returns early. + /// + /// Important note: The `Break` type should be kept as small as possible to prevent + /// stack overflow during recursion. If you need to return an error, consider + /// boxing it with `Box` to minimize stack usage. type Break; /// Invoked for any queries that appear in the AST before visiting children @@ -233,6 +237,16 @@ pub trait Visitor { fn post_visit_statement(&mut self, _statement: &Statement) -> ControlFlow { ControlFlow::Continue(()) } + + /// Invoked for any Value that appear in the AST before visiting children + fn pre_visit_value(&mut self, _value: &Value) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any Value that appear in the AST after visiting children + fn post_visit_value(&mut self, _value: &Value) -> ControlFlow { + ControlFlow::Continue(()) + } } /// A visitor that can be used to mutate an AST tree. @@ -280,6 +294,10 @@ pub trait Visitor { /// ``` pub trait VisitorMut { /// Type returned when the recursion returns early. + /// + /// Important note: The `Break` type should be kept as small as possible to prevent + /// stack overflow during recursion. If you need to return an error, consider + /// boxing it with `Box` to minimize stack usage. type Break; /// Invoked for any queries that appear in the AST before visiting children @@ -337,6 +355,16 @@ pub trait VisitorMut { fn post_visit_statement(&mut self, _statement: &mut Statement) -> ControlFlow { ControlFlow::Continue(()) } + + /// Invoked for any value that appear in the AST before visiting children + fn pre_visit_value(&mut self, _value: &mut Value) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any statements that appear in the AST after visiting children + fn post_visit_value(&mut self, _value: &mut Value) -> ControlFlow { + ControlFlow::Continue(()) + } } struct RelationVisitor(F); @@ -403,7 +431,7 @@ where /// ``` /// # use sqlparser::parser::Parser; /// # use sqlparser::dialect::GenericDialect; -/// # use sqlparser::ast::{ObjectName, visit_relations_mut}; +/// # use sqlparser::ast::{ObjectName, ObjectNamePart, Ident, visit_relations_mut}; /// # use core::ops::ControlFlow; /// let sql = "SELECT a FROM foo"; /// let mut statements = Parser::parse_sql(&GenericDialect{}, sql) @@ -411,7 +439,7 @@ where /// /// // visit statements, renaming table foo to bar /// visit_relations_mut(&mut statements, |table| { -/// table.0[0].value = table.0[0].value.replace("foo", "bar"); +/// table.0[0] = ObjectNamePart::Identifier(Ident::new("bar")); /// ControlFlow::<()>::Continue(()) /// }); /// @@ -503,7 +531,7 @@ where /// // Remove all select limits in sub-queries /// visit_expressions_mut(&mut statements, |expr| { /// if let Expr::Subquery(q) = expr { -/// q.limit = None +/// q.limit_clause = None; /// } /// ControlFlow::<()>::Continue(()) /// }); @@ -527,9 +555,9 @@ where /// /// visit_expressions_mut(&mut statements, |expr| { /// if matches!(expr, Expr::Identifier(col_name) if col_name.value == "x") { -/// let old_expr = std::mem::replace(expr, Expr::Value(Value::Null)); +/// let old_expr = std::mem::replace(expr, Expr::value(Value::Null)); /// *expr = Expr::Function(Function { -/// name: ObjectName(vec![Ident::new("f")]), +/// name: ObjectName::from(vec![Ident::new("f")]), /// uses_odbc_syntax: false, /// args: FunctionArguments::List(FunctionArgumentList { /// duplicate_treatment: None, @@ -627,7 +655,7 @@ where /// // Remove all select limits in outer statements (not in sub-queries) /// visit_statements_mut(&mut statements, |stmt| { /// if let Statement::Query(q) = stmt { -/// q.limit = None +/// q.limit_clause = None; /// } /// ControlFlow::<()>::Continue(()) /// }); @@ -647,6 +675,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::ast::Statement; use crate::dialect::GenericDialect; use crate::parser::Parser; use crate::tokenizer::Tokenizer; @@ -720,7 +749,7 @@ mod tests { } } - fn do_visit(sql: &str) -> Vec { + 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) @@ -728,9 +757,9 @@ mod tests { .parse_statement() .unwrap(); - let mut visitor = TestVisitor::default(); - s.visit(&mut visitor); - visitor.visited + let flow = s.visit(visitor); + assert_eq!(flow, ControlFlow::Continue(())); + s } #[test] @@ -863,6 +892,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'", @@ -889,8 +920,9 @@ mod tests { ), ]; for (sql, expected) in tests { - let actual = do_visit(sql); - let actual: Vec<_> = actual.iter().map(|x| x.as_str()).collect(); + let mut visitor = TestVisitor::default(); + let _ = do_visit(sql, &mut visitor); + let actual: Vec<_> = visitor.visited.iter().map(|x| x.as_str()).collect(); assert_eq!(actual, expected) } } @@ -904,10 +936,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(); @@ -917,6 +949,72 @@ mod tests { .unwrap(); let mut visitor = QuickVisitor {}; - s.visit(&mut visitor); + let flow = s.visit(&mut visitor); + assert_eq!(flow, ControlFlow::Continue(())); + } +} + +#[cfg(test)] +mod visit_mut_tests { + use crate::ast::{Statement, Value, VisitMut, VisitorMut}; + use crate::dialect::GenericDialect; + use crate::parser::Parser; + use crate::tokenizer::Tokenizer; + use core::ops::ControlFlow; + + #[derive(Default)] + struct MutatorVisitor { + index: u64, + } + + impl VisitorMut for MutatorVisitor { + type Break = (); + + fn pre_visit_value(&mut self, value: &mut Value) -> ControlFlow { + self.index += 1; + *value = Value::SingleQuotedString(format!("REDACTED_{}", self.index)); + ControlFlow::Continue(()) + } + + fn post_visit_value(&mut self, _value: &mut Value) -> ControlFlow { + ControlFlow::Continue(()) + } + } + + 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) + .with_tokens(tokens) + .parse_statement() + .unwrap(); + + let flow = s.visit(visitor); + assert_eq!(flow, ControlFlow::Continue(())); + s + } + + #[test] + fn test_value_redact() { + let tests = vec![ + ( + concat!( + "SELECT * FROM monthly_sales ", + "PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ", + "ORDER BY EMPID" + ), + concat!( + "SELECT * FROM monthly_sales ", + "PIVOT(SUM(a.amount) FOR a.MONTH IN ('REDACTED_1', 'REDACTED_2', 'REDACTED_3', 'REDACTED_4')) AS p (c, d) ", + "ORDER BY EMPID" + ), + ), + ]; + + for (sql, expected) in tests { + let mut visitor = MutatorVisitor::default(); + let mutated = do_visit_mut(sql, &mut visitor); + assert_eq!(mutated.to_string(), expected) + } } } diff --git a/src/dialect/ansi.rs b/src/dialect/ansi.rs index 32ba7b32a..ec3c095be 100644 --- a/src/dialect/ansi.rs +++ b/src/dialect/ansi.rs @@ -33,4 +33,9 @@ impl Dialect for AnsiDialect { fn require_interval_qualifier(&self) -> bool { true } + + /// The SQL standard explicitly states that block comments nest. + fn supports_nested_comments(&self) -> bool { + true + } } diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 66d7d2061..27fd3cca3 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -15,14 +15,53 @@ // specific language governing permissions and limitations // under the License. +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. +const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ + Keyword::WITH, + Keyword::SELECT, + Keyword::WHERE, + Keyword::GROUP, + Keyword::HAVING, + Keyword::ORDER, + Keyword::LATERAL, + Keyword::LIMIT, + Keyword::FETCH, + Keyword::UNION, + Keyword::EXCEPT, + Keyword::INTERSECT, + Keyword::FROM, + Keyword::INTO, + Keyword::END, +]; /// A [`Dialect`] for [Google Bigquery](https://cloud.google.com/bigquery/) #[derive(Debug, Default)] pub struct BigQueryDialect; impl Dialect for BigQueryDialect { - // See https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers + fn parse_statement(&self, parser: &mut Parser) -> Option> { + 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 fn is_delimited_identifier_start(&self, ch: char) -> bool { ch == '`' } @@ -31,8 +70,16 @@ impl Dialect for BigQueryDialect { true } + /// See + fn supports_column_definition_trailing_commas(&self) -> bool { + true + } + fn is_identifier_start(&self, ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' + // BigQuery supports `@@foo.bar` variable syntax in its procedural language. + // https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend + || ch == '@' } fn is_identifier_part(&self, ch: char) -> bool { @@ -77,4 +124,36 @@ impl Dialect for BigQueryDialect { fn supports_struct_literal(&self) -> bool { true } + + /// See + fn supports_select_expr_star(&self) -> bool { + true + } + + /// See + fn supports_execute_immediate(&self) -> bool { + true + } + + // See + fn supports_timestamp_versioning(&self) -> bool { + true + } + + // See + fn supports_group_by_expr(&self) -> bool { + true + } + + fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + !RESERVED_FOR_COLUMN_ALIAS.contains(kw) + } + + fn supports_pipe_operator(&self) -> bool { + true + } + + fn supports_create_table_multi_schema_info_sources(&self) -> bool { + true + } } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 884dfcbcb..bdac1f57b 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -59,6 +59,10 @@ impl Dialect for ClickHouseDialect { true } + fn supports_numeric_literal_underscores(&self) -> bool { + true + } + // ClickHouse uses this for some FORMAT expressions in `INSERT` context, e.g. when inserting // with FORMAT JSONEachRow a raw JSON key-value expression is valid and expected. // @@ -66,4 +70,34 @@ impl Dialect for ClickHouseDialect { fn supports_dictionary_syntax(&self) -> bool { true } + + /// See + fn supports_lambda_functions(&self) -> bool { + true + } + + fn supports_from_first_select(&self) -> bool { + true + } + + /// See + fn supports_order_by_all(&self) -> bool { + true + } + + // See + fn supports_group_by_expr(&self) -> bool { + true + } + + /// See + fn supports_group_by_with_modifier(&self) -> bool { + true + } + + /// Supported since 2020. + /// See + fn supports_nested_comments(&self) -> bool { + true + } } diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index a3476b1b8..c5d5f9740 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -64,4 +64,14 @@ impl Dialect for DatabricksDialect { fn supports_struct_literal(&self) -> bool { true } + + /// See + fn supports_nested_comments(&self) -> bool { + true + } + + /// See + fn supports_group_by_with_modifier(&self) -> bool { + true + } } diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index a2699d850..f08d827b9 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -65,6 +65,16 @@ impl Dialect for DuckDbDialect { true } + /// See + fn supports_lambda_functions(&self) -> bool { + true + } + + /// Returns true if this dialect allows the `EXTRACT` function to use single quotes in the part being extracted. + fn allow_extract_single_quotes(&self) -> bool { + true + } + // DuckDB is compatible with PostgreSQL syntax for this statement, // although not all features may be implemented. fn supports_explain_with_utility_options(&self) -> bool { @@ -75,4 +85,28 @@ impl Dialect for DuckDbDialect { fn supports_load_extension(&self) -> bool { true } + + // See DuckDB + fn supports_array_typedef_with_brackets(&self) -> bool { + true + } + + fn supports_from_first_select(&self) -> bool { + true + } + + /// See DuckDB + 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 d696861b5..dffc5b527 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -48,6 +48,14 @@ impl Dialect for GenericDialect { true } + fn supports_group_by_with_modifier(&self) -> bool { + true + } + + fn supports_left_associative_joins_without_parens(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } @@ -56,6 +64,10 @@ impl Dialect for GenericDialect { true } + fn supports_pipe_operator(&self) -> bool { + true + } + fn supports_start_transaction_modifier(&self) -> bool { true } @@ -104,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 } @@ -139,4 +159,40 @@ impl Dialect for GenericDialect { fn supports_user_host_grantee(&self) -> bool { true } + + fn supports_string_escape_constant(&self) -> bool { + true + } + + fn supports_array_typedef_with_brackets(&self) -> bool { + true + } + + fn supports_match_against(&self) -> bool { + true + } + + fn supports_set_names(&self) -> bool { + true + } + + fn supports_comma_separated_set_assignments(&self) -> bool { + true + } + + 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/hive.rs b/src/dialect/hive.rs index 80f44cf7c..3e15d395b 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -52,18 +52,23 @@ impl Dialect for HiveDialect { true } - /// See Hive + /// See fn supports_bang_not_operator(&self) -> bool { true } - /// See Hive + /// See fn supports_load_data(&self) -> bool { true } - /// See Hive + /// See fn supports_table_sample_before_alias(&self) -> bool { true } + + /// See + fn supports_group_by_with_modifier(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 64dbc4b1b..ef4e1cdde 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}; @@ -201,6 +201,33 @@ pub trait Dialect: Debug + Any { false } + /// Determine whether the dialect strips the backslash when escaping LIKE wildcards (%, _). + /// + /// [MySQL] has a special case when escaping single quoted strings which leaves these unescaped + /// so they can be used in LIKE patterns without double-escaping (as is necessary in other + /// escaping dialects, such as [Snowflake]). Generally, special characters have escaping rules + /// causing them to be replaced with a different byte sequences (e.g. `'\0'` becoming the zero + /// byte), and the default if an escaped character does not have a specific escaping rule is to + /// strip the backslash (e.g. there is no rule for `h`, so `'\h' = 'h'`). MySQL's special case + /// for ignoring LIKE wildcard escapes is to *not* strip the backslash, so that `'\%' = '\\%'`. + /// This applies to all string literals though, not just those used in LIKE patterns. + /// + /// ```text + /// mysql> select '\_', hex('\\'), hex('_'), hex('\_'); + /// +----+-----------+----------+-----------+ + /// | \_ | hex('\\') | hex('_') | hex('\_') | + /// +----+-----------+----------+-----------+ + /// | \_ | 5C | 5F | 5C5F | + /// +----+-----------+----------+-----------+ + /// 1 row in set (0.00 sec) + /// ``` + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/string-literals.html + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/functions/like#usage-notes + fn ignores_wildcard_escapes(&self) -> bool { + false + } + /// Determine if the dialect supports string literals with `U&` prefix. /// This is used to specify Unicode code points in string literals. /// For example, in PostgreSQL, the following is a valid string literal: @@ -245,11 +272,60 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialects supports `GROUP BY` modifiers prefixed by a `WITH` keyword. + /// Example: `GROUP BY value WITH ROLLUP`. + fn supports_group_by_with_modifier(&self) -> bool { + 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 + } + + /// Returns true if the dialect supports a join specification on CROSS JOIN. + fn supports_cross_join_constraint(&self) -> bool { + false + } + /// Returns true if the dialect supports CONNECT BY. fn supports_connect_by(&self) -> bool { false } + /// Returns true if the dialect supports `EXECUTE IMMEDIATE` statements. + fn supports_execute_immediate(&self) -> bool { + false + } + /// Returns true if the dialect supports the MATCH_RECOGNIZE operation. fn supports_match_recognize(&self) -> bool { false @@ -304,6 +380,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports numbers containing underscores, e.g. `10_000_000` + fn supports_numeric_literal_underscores(&self) -> bool { + false + } + /// Returns true if the dialects supports specifying null treatment /// as part of a window function's parameter list as opposed /// to after the parameter list. @@ -341,22 +422,23 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if the dialect supports method calls, for example: + /// Returns true if the dialect supports multiple variable assignment + /// using parentheses in a `SET` variable declaration. /// /// ```sql - /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + /// SET (variable[, ...]) = (expression[, ...]); /// ``` - fn supports_methods(&self) -> bool { + fn supports_parenthesized_set_variables(&self) -> bool { false } - /// Returns true if the dialect supports multiple variable assignment - /// using parentheses in a `SET` variable declaration. + /// Returns true if the dialect supports multiple `SET` statements + /// in a single statement. /// /// ```sql - /// SET (variable[, ...]) = (expression[, ...]); + /// SET variable = expression [, variable = expression]; /// ``` - fn supports_parenthesized_set_variables(&self) -> bool { + fn supports_comma_separated_set_assignments(&self) -> bool { false } @@ -399,17 +481,30 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports concatenating of string literal + /// Example: `SELECT 'Hello ' "world" => SELECT 'Hello world'` + fn supports_string_literal_concatenation(&self) -> bool { + false + } + /// Does the dialect support trailing commas in the projection list? fn supports_projection_trailing_commas(&self) -> bool { self.supports_trailing_commas() } /// Returns true if the dialect supports trailing commas in the `FROM` clause of a `SELECT` statement. - /// /// Example: `SELECT 1 FROM T, U, LIMIT 1` + /// Example: `SELECT 1 FROM T, U, LIMIT 1` fn supports_from_trailing_commas(&self) -> bool { false } + /// Returns true if the dialect supports trailing commas in the + /// column definitions list of a `CREATE` statement. + /// Example: `CREATE TABLE T (x INT, y TEXT,)` + fn supports_column_definition_trailing_commas(&self) -> bool { + false + } + /// Returns true if the dialect supports double dot notation for object names /// /// Example @@ -440,11 +535,79 @@ pub trait Dialect: Debug + Any { false } + /// Return true if the dialect supports wildcard expansion on + /// arbitrary expressions in projections. + /// + /// Example: + /// ```sql + /// SELECT STRUCT('foo').* FROM T + /// ``` + fn supports_select_expr_star(&self) -> bool { + false + } + + /// Return true if the dialect supports "FROM-first" selects. + /// + /// Example: + /// ```sql + /// FROM table + /// SELECT * + /// ``` + fn supports_from_first_select(&self) -> bool { + 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 } + /// Does the dialect support the `MATCH() AGAINST()` syntax? + fn supports_match_against(&self) -> bool { + 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. @@ -490,7 +653,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)), @@ -522,9 +685,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)), @@ -533,10 +706,14 @@ 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::Eq + Token::Period => Ok(p!(Period)), + Token::Assignment + | Token::Eq | Token::Lt | Token::LtEq | Token::Neq @@ -552,18 +729,34 @@ pub trait Dialect: Debug + Any { | Token::ExclamationMarkDoubleTilde | Token::ExclamationMarkDoubleTildeAsterisk | Token::Spaceship => Ok(p!(Eq)), - Token::Pipe => Ok(p!(Pipe)), + Token::Pipe + | Token::QuestionMarkDash + | Token::DoubleSharp + | Token::Overlap + | Token::AmpersandLeftAngleBracket + | Token::AmpersandRightAngleBracket + | Token::QuestionMarkDashVerticalBar + | Token::AmpersandLeftAngleBracketVerticalBar + | Token::VerticalBarAmpersandRightAngleBracket + | Token::TwoWayArrow + | Token::LeftAngleBracketCaret + | Token::RightAngleBracketCaret + | Token::QuestionMarkSharp + | Token::QuestionMarkDoubleVerticalBar + | Token::QuestionPipe + | Token::TildeEqual + | Token::AtSign + | Token::ShiftLeftVerticalBar + | Token::VerticalBarShiftRight => Ok(p!(Pipe)), Token::Caret | Token::Sharp | Token::ShiftRight | Token::ShiftLeft => Ok(p!(Caret)), Token::Ampersand => Ok(p!(Ampersand)), Token::Plus | Token::Minus => Ok(p!(PlusMinus)), Token::Mul | Token::Div | Token::DuckIntDiv | Token::Mod | Token::StringConcat => { Ok(p!(MulDivModOp)) } - Token::DoubleColon - | Token::ExclamationMark - | Token::LBracket - | Token::Overlap - | Token::CaretAt => Ok(p!(DoubleColon)), + Token::DoubleColon | Token::ExclamationMark | Token::LBracket | Token::CaretAt => { + Ok(p!(DoubleColon)) + } Token::Arrow | Token::LongArrow | Token::HashArrow @@ -575,7 +768,6 @@ pub trait Dialect: Debug + Any { | Token::AtAt | Token::Question | Token::QuestionAnd - | Token::QuestionPipe | Token::CustomBinaryOperator(_) => Ok(p!(PgOther)), _ => Ok(self.prec_unknown()), } @@ -609,6 +801,7 @@ pub trait Dialect: Debug + Any { /// Uses (APPROXIMATELY) as a reference fn prec_value(&self, prec: Precedence) -> u8 { match prec { + Precedence::Period => 100, Precedence::DoubleColon => 50, Precedence::AtTz => 41, Precedence::MulDivModOp => 40, @@ -781,10 +974,15 @@ 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 @@ -820,6 +1018,195 @@ pub trait Dialect: Debug + Any { fn supports_set_stmt_without_operator(&self) -> bool { false } + + /// Returns true if the specified keyword should be parsed as a column identifier. + /// See [keywords::RESERVED_FOR_COLUMN_ALIAS] + fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw) + } + + /// Returns true if the specified keyword should be parsed as a select item alias. + /// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided + /// to enable looking ahead if needed. + fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + 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 || self.is_table_alias(kw, parser) + } + + /// Returns true if this dialect supports querying historical table data + /// by specifying which version of the data to query. + fn supports_timestamp_versioning(&self) -> bool { + false + } + + /// Returns true if this dialect supports the E'...' syntax for string literals + /// + /// Postgres: + fn supports_string_escape_constant(&self) -> bool { + false + } + + /// Returns true if the dialect supports the table hints in the `FROM` clause. + fn supports_table_hints(&self) -> bool { + false + } + + /// Returns true if this dialect requires a whitespace character after `--` to start a single line comment. + /// + /// MySQL: + /// e.g. UPDATE account SET balance=balance--1 + // WHERE account_id=5752 ^^^ will be interpreted as two minus signs instead of a comment + fn requires_single_line_comment_whitespace(&self) -> bool { + false + } + + /// Returns true if the dialect supports array type definition with brackets with + /// an optional size. For example: + /// ```CREATE TABLE my_table (arr1 INT[], arr2 INT[3])``` + /// ```SELECT x::INT[]``` + fn supports_array_typedef_with_brackets(&self) -> bool { + false + } + /// Returns true if the dialect supports geometric types. + /// + /// Postgres: + /// e.g. @@ circle '((0,0),10)' + fn supports_geometric_types(&self) -> bool { + false + } + + /// Returns true if the dialect supports `ORDER BY ALL`. + /// `ALL` which means all columns of the SELECT clause. + /// + /// For example: ```SELECT * FROM addresses ORDER BY ALL;```. + fn supports_order_by_all(&self) -> bool { + false + } + + /// Returns true if the dialect supports `SET NAMES [COLLATE ]`. + /// + /// - [MySQL](https://dev.mysql.com/doc/refman/8.4/en/set-names.html) + /// - [PostgreSQL](https://www.postgresql.org/docs/17/sql-set.html) + /// + /// Note: Postgres doesn't support the `COLLATE` clause, but we permissively parse it anyway. + 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 @@ -827,6 +1214,7 @@ pub trait Dialect: Debug + Any { /// higher number -> higher precedence #[derive(Debug, Clone, Copy)] pub enum Precedence { + Period, DoubleColon, AtTz, MulDivModOp, diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 67a648944..e1902b389 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -15,7 +15,19 @@ // specific language governing permissions and limitations // under the License. +use crate::ast::helpers::attached_token::AttachedToken; +use crate::ast::{ + BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, CreateTrigger, + GranteesType, IfStatement, Statement, +}; use crate::dialect::Dialect; +use crate::keywords::{self, Keyword}; +use crate::parser::{Parser, ParserError}; +use crate::tokenizer::Token; +#[cfg(not(feature = "std"))] +use alloc::{vec, vec::Vec}; + +const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[Keyword::IF, Keyword::ELSE]; /// A [`Dialect`] for [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/) #[derive(Debug)] @@ -40,12 +52,20 @@ 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 { true } + fn supports_outer_join_operator(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } @@ -63,10 +83,6 @@ impl Dialect for MsSqlDialect { false } - fn supports_methods(&self) -> bool { - true - } - fn supports_named_fn_args_with_colon_operator(&self) -> bool { true } @@ -82,6 +98,7 @@ impl Dialect for MsSqlDialect { fn supports_start_transaction_modifier(&self) -> bool { true } + fn supports_end_transaction_modifier(&self) -> bool { true } @@ -90,4 +107,196 @@ impl Dialect for MsSqlDialect { fn supports_set_stmt_without_operator(&self) -> bool { true } + + /// See: + fn supports_timestamp_versioning(&self) -> bool { + true + } + + /// See + fn supports_nested_comments(&self) -> bool { + true + } + + /// See + fn supports_object_name_double_dot_notation(&self) -> bool { + 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) + } + + 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 + } + } +} + +impl MsSqlDialect { + /// ```sql + /// IF boolean_expression + /// { sql_statement | statement_block } + /// [ ELSE + /// { sql_statement | statement_block } ] + /// ``` + fn parse_if_stmt(&self, parser: &mut Parser) -> Result { + let if_token = parser.expect_keyword(Keyword::IF)?; + + let condition = parser.parse_expr()?; + + let if_block = if parser.peek_keyword(Keyword::BEGIN) { + let begin_token = parser.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(parser, Some(Keyword::END))?; + let end_token = parser.expect_keyword(Keyword::END)?; + ConditionalStatementBlock { + start_token: AttachedToken(if_token), + condition: Some(condition), + then_token: None, + conditional_statements: ConditionalStatements::BeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + }), + } + } else { + let stmt = parser.parse_statement()?; + ConditionalStatementBlock { + start_token: AttachedToken(if_token), + condition: Some(condition), + then_token: None, + conditional_statements: ConditionalStatements::Sequence { + statements: vec![stmt], + }, + } + }; + + let mut prior_statement_ended_with_semi_colon = false; + while let Token::SemiColon = parser.peek_token_ref().token { + parser.advance_token(); + prior_statement_ended_with_semi_colon = true; + } + + let mut else_block = None; + if parser.peek_keyword(Keyword::ELSE) { + let else_token = parser.expect_keyword(Keyword::ELSE)?; + if parser.peek_keyword(Keyword::BEGIN) { + let begin_token = parser.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(parser, Some(Keyword::END))?; + let end_token = parser.expect_keyword(Keyword::END)?; + else_block = Some(ConditionalStatementBlock { + start_token: AttachedToken(else_token), + condition: None, + then_token: None, + conditional_statements: ConditionalStatements::BeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + }), + }); + } else { + let stmt = parser.parse_statement()?; + else_block = Some(ConditionalStatementBlock { + start_token: AttachedToken(else_token), + condition: None, + then_token: None, + conditional_statements: ConditionalStatements::Sequence { + statements: vec![stmt], + }, + }); + } + } else if prior_statement_ended_with_semi_colon { + parser.prev_token(); + } + + Ok(IfStatement { + if_block, + else_block, + elseif_blocks: Vec::new(), + end_token: None, + } + .into()) + } + + /// 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(CreateTrigger { + or_alter, + temporary: false, + or_replace: false, + is_constraint: false, + name, + period: Some(period), + period_before_table: false, + events, + table_name, + referenced_table_name: None, + referencing: Vec::new(), + trigger_object: None, + condition: None, + exec_body: None, + statements_as: true, + statements, + characteristics: None, + } + .into()) + } + + /// Parse a sequence of statements, optionally separated by semicolon. + /// + /// Stops parsing when reaching EOF or the given keyword. + fn parse_statement_list( + &self, + parser: &mut Parser, + terminal_keyword: Option, + ) -> Result, ParserError> { + let mut stmts = Vec::new(); + loop { + if let Token::EOF = parser.peek_token_ref().token { + break; + } + if let Some(term) = terminal_keyword { + if parser.peek_keyword(term) { + break; + } + } + stmts.push(parser.parse_statement()?); + while let Token::SemiColon = parser.peek_token_ref().token { + parser.advance_token(); + } + } + Ok(stmts) + } } diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 535b4298a..8d2a5ad4b 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -25,6 +25,15 @@ use crate::{ parser::{Parser, ParserError}, }; +use super::keywords; + +const RESERVED_FOR_TABLE_ALIAS_MYSQL: &[Keyword] = &[ + Keyword::USE, + Keyword::IGNORE, + Keyword::FORCE, + Keyword::STRAIGHT_JOIN, +]; + /// A [`Dialect`] for [MySQL](https://www.mysql.com/) #[derive(Debug)] pub struct MySqlDialect {} @@ -34,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 { @@ -58,6 +71,15 @@ impl Dialect for MySqlDialect { true } + /// see + fn supports_string_literal_concatenation(&self) -> bool { + true + } + + fn ignores_wildcard_escapes(&self) -> bool { + true + } + fn supports_numeric_prefix(&self) -> bool { true } @@ -111,6 +133,40 @@ impl Dialect for MySqlDialect { fn supports_user_host_grantee(&self) -> bool { true } + + fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { + explicit + || (!keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) + && !RESERVED_FOR_TABLE_ALIAS_MYSQL.contains(kw)) + } + + fn supports_table_hints(&self) -> bool { + true + } + + fn requires_single_line_comment_whitespace(&self) -> bool { + true + } + + fn supports_match_against(&self) -> bool { + true + } + + fn supports_set_names(&self) -> bool { + true + } + + fn supports_comma_separated_set_assignments(&self) -> bool { + true + } + + fn supports_data_type_signed_suffix(&self) -> bool { + true + } + + fn supports_cross_join_constraint(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 170b0a7c9..e861cc515 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -28,7 +28,6 @@ // limitations under the License. use log::debug; -use crate::ast::{ObjectName, Statement, UserDefinedTypeRepresentation}; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -38,6 +37,7 @@ use crate::tokenizer::Token; #[derive(Debug)] pub struct PostgreSqlDialect {} +const PERIOD_PREC: u8 = 200; const DOUBLE_COLON_PREC: u8 = 140; const BRACKET_PREC: u8 = 130; const COLLATE_PREC: u8 = 120; @@ -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 @@ -135,15 +140,6 @@ impl Dialect for PostgreSqlDialect { } } - fn parse_statement(&self, parser: &mut Parser) -> Option> { - if parser.parse_keyword(Keyword::CREATE) { - parser.prev_token(); // unconsume the CREATE in case we don't end up parsing anything - parse_create(parser) - } else { - None - } - } - fn supports_filter_during_aggregation(&self) -> bool { true } @@ -154,6 +150,7 @@ impl Dialect for PostgreSqlDialect { fn prec_value(&self, prec: Precedence) -> u8 { match prec { + Precedence::Period => PERIOD_PREC, Precedence::DoubleColon => DOUBLE_COLON_PREC, Precedence::AtTz => AT_TZ_PREC, Precedence::MulDivModOp => MUL_DIV_MOD_OP_PREC, @@ -245,38 +242,42 @@ impl Dialect for PostgreSqlDialect { fn supports_nested_comments(&self) -> bool { true } -} -pub fn parse_create(parser: &mut Parser) -> Option> { - let name = parser.maybe_parse(|parser| -> Result { - parser.expect_keyword_is(Keyword::CREATE)?; - parser.expect_keyword_is(Keyword::TYPE)?; - let name = parser.parse_object_name(false)?; - parser.expect_keyword_is(Keyword::AS)?; - parser.expect_keyword_is(Keyword::ENUM)?; - Ok(name) - }); - - match name { - Ok(name) => name.map(|name| parse_create_type_as_enum(parser, name)), - Err(e) => Some(Err(e)), + fn supports_string_escape_constant(&self) -> bool { + true + } + + fn supports_numeric_literal_underscores(&self) -> bool { + true + } + + /// See: + fn supports_array_typedef_with_brackets(&self) -> bool { + true } -} -// https://www.postgresql.org/docs/current/sql-createtype.html -pub fn parse_create_type_as_enum( - parser: &mut Parser, - name: ObjectName, -) -> Result { - if !parser.consume_token(&Token::LParen) { - return parser.expected("'(' after CREATE TYPE AS ENUM", parser.peek_token()); + fn supports_geometric_types(&self) -> bool { + true + } + + fn supports_set_names(&self) -> bool { + true + } + + fn supports_alter_column_type_using(&self) -> bool { + true } - let labels = parser.parse_comma_separated0(|p| p.parse_identifier(), Token::RParen)?; - parser.expect_token(&Token::RParen)?; + /// Postgres supports `NOTNULL` as an alias for `IS NOT NULL` + /// See: + fn supports_notnull_operator(&self) -> bool { + true + } - Ok(Statement::CreateType { - name, - representation: UserDefinedTypeRepresentation::Enum { labels }, - }) + /// [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 55405ba53..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 == '#' } @@ -109,4 +111,36 @@ impl Dialect for RedshiftSqlDialect { fn supports_partiql(&self) -> bool { true } + + fn supports_string_escape_constant(&self) -> bool { + true + } + + fn supports_geometric_types(&self) -> bool { + true + } + + fn supports_array_typedef_with_brackets(&self) -> bool { + true + } + + fn allow_extract_single_quotes(&self) -> bool { + true + } + + 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 6b8380d63..bb0d4f16b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -17,21 +17,30 @@ #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; +use crate::ast::helpers::attached_token::AttachedToken; +use crate::ast::helpers::key_value_options::{ + KeyValueOption, KeyValueOptionKind, 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::{ - DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, FileStagingCommand, - StageLoadSelectItem, StageParamsObject, + FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ - ColumnOption, ColumnPolicy, ColumnPolicyProperty, Ident, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName, - RowAccessPolicy, Statement, TagsColumnOption, WrappedCollection, + AlterTable, AlterTableOperation, AlterTableType, CatalogSyncNamespaceMode, ColumnOption, + ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTableLikeKind, + DollarQuotedString, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, + IdentityPropertyKind, IdentityPropertyOrder, InitializeKind, ObjectName, ObjectNamePart, + RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, + StorageSerializationPolicy, TagsColumnOption, Value, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; -use crate::parser::{Parser, ParserError}; +use crate::parser::{IsOptional, Parser, ParserError}; use crate::tokenizer::Token; #[cfg(not(feature = "std"))] +use alloc::boxed::Box; +#[cfg(not(feature = "std"))] use alloc::string::String; #[cfg(not(feature = "std"))] use alloc::vec::Vec; @@ -40,6 +49,83 @@ use alloc::{format, vec}; use super::keywords::RESERVED_FOR_IDENTIFIER; +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; @@ -83,10 +169,20 @@ impl Dialect for SnowflakeDialect { true } + /// See + fn supports_outer_join_operator(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } + /// See + fn supports_execute_immediate(&self) -> bool { + true + } + fn supports_match_recognize(&self) -> bool { true } @@ -116,6 +212,25 @@ 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::DYNAMIC, Keyword::TABLE]) { + // ALTER DYNAMIC TABLE + return Some(parse_alter_dynamic_table(parser)); + } + + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { + // ALTER SESSION + let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { + Some(Keyword::SET) => true, + Some(Keyword::UNSET) => false, + _ => return Some(parser.expected("SET or UNSET", parser.peek_token())), + }; + return Some(parse_alter_session(parser, set)); + } + if parser.parse_keyword(Keyword::CREATE) { // possibly CREATE STAGE //[ OR REPLACE ] @@ -127,19 +242,24 @@ impl Dialect for SnowflakeDialect { _ => None, }; + let dynamic = parser.parse_keyword(Keyword::DYNAMIC); + let mut temporary = false; let mut volatile = false; let mut transient = false; + let mut iceberg = false; match parser.parse_one_of_keywords(&[ Keyword::TEMP, Keyword::TEMPORARY, Keyword::VOLATILE, Keyword::TRANSIENT, + Keyword::ICEBERG, ]) { Some(Keyword::TEMP | Keyword::TEMPORARY) => temporary = true, Some(Keyword::VOLATILE) => volatile = true, Some(Keyword::TRANSIENT) => transient = true, + Some(Keyword::ICEBERG) => iceberg = true, _ => {} } @@ -148,8 +268,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, 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; @@ -178,6 +300,19 @@ impl Dialect for SnowflakeDialect { return Some(parse_file_staging_command(kw, parser)); } + if parser.parse_keyword(Keyword::SHOW) { + let terse = parser.parse_keyword(Keyword::TERSE); + if parser.parse_keyword(Keyword::OBJECTS) { + return Some(parse_show_objects(terse, parser)); + } + //Give back Keyword::TERSE + if terse { + parser.prev_token(); + } + //Give back Keyword::SHOW + parser.prev_token(); + } + None } @@ -238,6 +373,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 @@ -251,6 +390,205 @@ impl Dialect for SnowflakeDialect { fn supports_partiql(&self) -> bool { true } + + 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. `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 + // `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 + } + + // Reserved keywords by the Snowflake dialect, which seem to be less strictive + // than what is listed in `keywords::RESERVED_FOR_COLUMN_ALIAS`. The following + // keywords were tested with the this statement: `SELECT 1 `. + Keyword::FROM + | Keyword::GROUP + | Keyword::HAVING + | Keyword::INTERSECT + | Keyword::INTO + | Keyword::MINUS + | Keyword::ORDER + | Keyword::SELECT + | Keyword::UNION + | Keyword::WHERE + | Keyword::WITH => false, + + // Any other word is considered an alias + _ => true, + } + } + + 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 + } + + /// See: + fn supports_group_by_expr(&self) -> bool { + true + } + + /// See: + 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 { @@ -273,14 +611,69 @@ fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result +fn parse_alter_dynamic_table(parser: &mut Parser) -> Result { + // Use parse_object_name(true) to support IDENTIFIER() function + let table_name = parser.parse_object_name(true)?; + + // Parse the operation (REFRESH, SUSPEND, or RESUME) + let operation = if parser.parse_keyword(Keyword::REFRESH) { + AlterTableOperation::Refresh + } else if parser.parse_keyword(Keyword::SUSPEND) { + AlterTableOperation::Suspend + } else if parser.parse_keyword(Keyword::RESUME) { + AlterTableOperation::Resume + } else { + return parser.expected( + "REFRESH, SUSPEND, or RESUME after ALTER DYNAMIC TABLE", + parser.peek_token(), + ); + }; + + let end_token = if parser.peek_token_ref().token == Token::SemiColon { + parser.peek_token_ref().clone() + } else { + parser.get_current_token().clone() + }; + + Ok(Statement::AlterTable(AlterTable { + name: table_name, + if_exists: false, + only: false, + operations: vec![operation], + location: None, + on_cluster: None, + table_type: Some(AlterTableType::Dynamic), + end_token: AttachedToken(end_token), + })) +} + +/// Parse snowflake alter session. +/// +fn parse_alter_session(parser: &mut Parser, set: bool) -> Result { + let session_options = parse_session_options(parser, set)?; + Ok(Statement::AlterSession { + set, + session_params: KeyValueOptions { + options: session_options, + delimiter: KeyValueOptionsDelimiter::Space, + }, + }) +} + /// Parse snowflake create table statement. /// +/// +#[allow(clippy::too_many_arguments)] pub fn parse_create_table( or_replace: bool, global: Option, temporary: bool, 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]); @@ -292,7 +685,9 @@ pub fn parse_create_table( .temporary(temporary) .transient(transient) .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 @@ -301,6 +696,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 { @@ -312,28 +709,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)?; @@ -341,29 +742,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)?; @@ -418,6 +801,74 @@ pub fn parse_create_table( let on_commit = Some(parser.parse_create_table_on_commit()?); builder = builder.on_commit(on_commit); } + Keyword::EXTERNAL_VOLUME => { + parser.expect_token(&Token::Eq)?; + builder.external_volume = Some(parser.parse_literal_string()?); + } + Keyword::CATALOG => { + parser.expect_token(&Token::Eq)?; + builder.catalog = Some(parser.parse_literal_string()?); + } + Keyword::BASE_LOCATION => { + parser.expect_token(&Token::Eq)?; + builder.base_location = Some(parser.parse_literal_string()?); + } + Keyword::CATALOG_SYNC => { + parser.expect_token(&Token::Eq)?; + builder.catalog_sync = Some(parser.parse_literal_string()?); + } + Keyword::STORAGE_SERIALIZATION_POLICY => { + parser.expect_token(&Token::Eq)?; + + 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); } @@ -428,21 +879,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; } @@ -451,10 +890,146 @@ 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( + "BASE_LOCATION is required for ICEBERG tables".to_string(), + )); + } 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 { + let next_token = parser.next_token(); + match &next_token.token { + Token::Word(w) => match w.keyword { + Keyword::COMPATIBLE => Ok(StorageSerializationPolicy::Compatible), + Keyword::OPTIMIZED => Ok(StorageSerializationPolicy::Optimized), + _ => parser.expected("storage_serialization_policy", next_token), + }, + _ => parser.expected("storage_serialization_policy", next_token), + } +} + pub fn parse_create_stage( or_replace: bool, temporary: bool, @@ -474,28 +1049,25 @@ 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, &[])?.options; } // [ 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, &[])?.options; } // [ 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, &[])?.options; } // [ comment ] if parser.parse_keyword(Keyword::COMMENT) { parser.expect_token(&Token::Eq)?; - comment = Some(match parser.next_token().token { - Token::SingleQuotedString(word) => Ok(word), - _ => parser.expected("a comment statement", parser.peek_token()), - }?) + comment = Some(parser.parse_comment_value()?); } Ok(Statement::CreateStage { @@ -504,14 +1076,17 @@ pub fn parse_create_stage( if_not_exists, name, stage_params, - directory_table_params: DataLoadingOptions { + directory_table_params: KeyValueOptions { options: directory_table_params, + delimiter: KeyValueOptionsDelimiter::Space, }, - file_format: DataLoadingOptions { + file_format: KeyValueOptions { options: file_format, + delimiter: KeyValueOptionsDelimiter::Space, }, - copy_options: DataLoadingOptions { + copy_options: KeyValueOptions { options: copy_options, + delimiter: KeyValueOptionsDelimiter::Space, }, comment, }) @@ -521,7 +1096,7 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result break, + Token::Whitespace(_) | Token::SemiColon => break, Token::Period => { parser.prev_token(); break; @@ -534,6 +1109,9 @@ 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::Minus => 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()), } @@ -552,7 +1130,7 @@ pub fn parse_snowflake_stage_name(parser: &mut Parser) -> Result { parser.prev_token(); @@ -561,40 +1139,76 @@ pub fn parse_snowflake_stage_name(parser: &mut Parser) -> Result` +/// and `COPY INTO ` which have different syntax. pub fn parse_copy_into(parser: &mut Parser) -> Result { - let into: ObjectName = parse_snowflake_stage_name(parser)?; + let kind = match parser.peek_token().token { + // Indicates an internal stage + Token::AtSign => CopyIntoSnowflakeKind::Location, + // Indicates an external stage, i.e. s3://, gcs:// or azure:// + Token::SingleQuotedString(s) if s.contains("://") => CopyIntoSnowflakeKind::Location, + _ => CopyIntoSnowflakeKind::Table, + }; + let mut files: Vec = vec![]; - let mut from_transformations: Option> = None; - let from_stage_alias; - let from_stage: ObjectName; - let stage_params: StageParamsObject; + let mut from_transformations: Option> = None; + let mut from_stage_alias = None; + let mut from_stage = None; + let mut stage_params = StageParamsObject { + url: None, + encryption: KeyValueOptions { + options: vec![], + delimiter: KeyValueOptionsDelimiter::Space, + }, + endpoint: None, + storage_integration: None, + credentials: KeyValueOptions { + options: vec![], + delimiter: KeyValueOptionsDelimiter::Space, + }, + }; + let mut from_query = None; + let mut partition = None; + let mut file_format = Vec::new(); + let mut pattern = None; + let mut validation_mode = None; + let mut copy_options = Vec::new(); + + let into: ObjectName = parse_snowflake_stage_name(parser)?; + if kind == CopyIntoSnowflakeKind::Location { + stage_params = parse_stage_params(parser)?; + } + + let into_columns = match &parser.peek_token().token { + Token::LParen => Some(parser.parse_parenthesized_column_list(IsOptional::Optional, true)?), + _ => None, + }; parser.expect_keyword_is(Keyword::FROM)?; - // check if data load transformations are present match parser.next_token().token { - Token::LParen => { - // data load with transformations + Token::LParen if kind == CopyIntoSnowflakeKind::Table => { + // Data load with transformations parser.expect_keyword_is(Keyword::SELECT)?; from_transformations = parse_select_items_for_data_load(parser)?; parser.expect_keyword_is(Keyword::FROM)?; - from_stage = parse_snowflake_stage_name(parser)?; + from_stage = Some(parse_snowflake_stage_name(parser)?); stage_params = parse_stage_params(parser)?; - // as - from_stage_alias = if parser.parse_keyword(Keyword::AS) { - Some(match parser.next_token().token { - Token::Word(w) => Ok(Ident::new(w.value)), - _ => parser.expected("stage alias", parser.peek_token()), - }?) - } else { - None - }; + // Parse an optional alias + from_stage_alias = parser + .maybe_parse_table_alias()? + .map(|table_alias| table_alias.name); + parser.expect_token(&Token::RParen)?; + } + Token::LParen if kind == CopyIntoSnowflakeKind::Location => { + // Data unload with a query + from_query = Some(parser.parse_query()?); parser.expect_token(&Token::RParen)?; } _ => { parser.prev_token(); - from_stage = parse_snowflake_stage_name(parser)?; + from_stage = Some(parse_snowflake_stage_name(parser)?); stage_params = parse_stage_params(parser)?; // as @@ -607,167 +1221,188 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { None }; } - }; + } - // [ files ] - if parser.parse_keyword(Keyword::FILES) { - parser.expect_token(&Token::Eq)?; - parser.expect_token(&Token::LParen)?; - let mut continue_loop = true; - while continue_loop { - continue_loop = false; + loop { + // FILE_FORMAT + if parser.parse_keyword(Keyword::FILE_FORMAT) { + parser.expect_token(&Token::Eq)?; + file_format = parser.parse_key_value_options(true, &[])?.options; + // PARTITION BY + } else if parser.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { + partition = Some(Box::new(parser.parse_expr()?)) + // FILES + } else if parser.parse_keyword(Keyword::FILES) { + parser.expect_token(&Token::Eq)?; + parser.expect_token(&Token::LParen)?; + let mut continue_loop = true; + while continue_loop { + continue_loop = false; + let next_token = parser.next_token(); + match next_token.token { + Token::SingleQuotedString(s) => files.push(s), + _ => parser.expected("file token", next_token)?, + }; + if parser.next_token().token.eq(&Token::Comma) { + continue_loop = true; + } else { + parser.prev_token(); // not a comma, need to go back + } + } + parser.expect_token(&Token::RParen)?; + // PATTERN + } else if parser.parse_keyword(Keyword::PATTERN) { + parser.expect_token(&Token::Eq)?; let next_token = parser.next_token(); - match next_token.token { - Token::SingleQuotedString(s) => files.push(s), - _ => parser.expected("file token", next_token)?, - }; - if parser.next_token().token.eq(&Token::Comma) { - continue_loop = true; - } else { - parser.prev_token(); // not a comma, need to go back + pattern = Some(match next_token.token { + Token::SingleQuotedString(s) => s, + _ => parser.expected("pattern", next_token)?, + }); + // VALIDATION MODE + } else if parser.parse_keyword(Keyword::VALIDATION_MODE) { + parser.expect_token(&Token::Eq)?; + validation_mode = Some(parser.next_token().token.to_string()); + // COPY OPTIONS + } else if parser.parse_keyword(Keyword::COPY_OPTIONS) { + parser.expect_token(&Token::Eq)?; + copy_options = parser.parse_key_value_options(true, &[])?.options; + } 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(parser.parse_key_value_option(&key)?), + _ => return parser.expected("another copy option, ; or EOF'", parser.peek_token()), } } - parser.expect_token(&Token::RParen)?; - } - - // [ pattern ] - let mut pattern = None; - if parser.parse_keyword(Keyword::PATTERN) { - parser.expect_token(&Token::Eq)?; - let next_token = parser.next_token(); - pattern = Some(match next_token.token { - Token::SingleQuotedString(s) => s, - _ => parser.expected("pattern", next_token)?, - }); - } - - // [ file_format] - let mut file_format = Vec::new(); - if parser.parse_keyword(Keyword::FILE_FORMAT) { - parser.expect_token(&Token::Eq)?; - file_format = parse_parentheses_options(parser)?; - } - - // [ copy_options ] - let mut copy_options = Vec::new(); - if parser.parse_keyword(Keyword::COPY_OPTIONS) { - parser.expect_token(&Token::Eq)?; - copy_options = parse_parentheses_options(parser)?; - } - - // [ VALIDATION_MODE ] - let mut validation_mode = None; - if parser.parse_keyword(Keyword::VALIDATION_MODE) { - parser.expect_token(&Token::Eq)?; - validation_mode = Some(parser.next_token().token.to_string()); } Ok(Statement::CopyIntoSnowflake { + kind, into, - from_stage, - from_stage_alias, + into_columns, + from_obj: from_stage, + from_obj_alias: from_stage_alias, stage_params, from_transformations, + from_query, files: if files.is_empty() { None } else { Some(files) }, pattern, - file_format: DataLoadingOptions { + file_format: KeyValueOptions { options: file_format, + delimiter: KeyValueOptionsDelimiter::Space, }, - copy_options: DataLoadingOptions { + copy_options: KeyValueOptions { options: copy_options, + delimiter: KeyValueOptionsDelimiter::Space, }, validation_mode, + partition, }) } fn parse_select_items_for_data_load( parser: &mut Parser, -) -> Result>, ParserError> { - // [.]$[.] [ , [.]$[.] ... ] - let mut select_items: Vec = vec![]; +) -> Result>, ParserError> { + let mut select_items: Vec = vec![]; loop { - let mut alias: Option = None; - let mut file_col_num: i32 = 0; - let mut element: Option = None; - let mut item_as: Option = None; + match parser.maybe_parse(parse_select_item_for_data_load)? { + // [.]$[.] [ , [.]$[.] ... ] + Some(item) => select_items.push(StageLoadSelectItemKind::StageLoadSelectItem(item)), + // Fallback, try to parse a standard SQL select item + None => select_items.push(StageLoadSelectItemKind::SelectItem( + parser.parse_select_item()?, + )), + } + if matches!(parser.peek_token_ref().token, Token::Comma) { + parser.advance_token(); + } else { + break; + } + } + Ok(Some(select_items)) +} - let next_token = parser.next_token(); - match next_token.token { +fn parse_select_item_for_data_load( + parser: &mut Parser, +) -> Result { + let mut alias: Option = None; + let mut file_col_num: i32 = 0; + let mut element: Option = None; + let mut item_as: Option = None; + + let next_token = parser.next_token(); + match next_token.token { + Token::Placeholder(w) => { + file_col_num = w.to_string().split_off(1).parse::().map_err(|e| { + ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}")) + })?; + Ok(()) + } + Token::Word(w) => { + alias = Some(Ident::new(w.value)); + Ok(()) + } + _ => parser.expected("alias or file_col_num", next_token), + }?; + + if alias.is_some() { + parser.expect_token(&Token::Period)?; + // now we get col_num token + let col_num_token = parser.next_token(); + match col_num_token.token { Token::Placeholder(w) => { file_col_num = w.to_string().split_off(1).parse::().map_err(|e| { ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}")) })?; Ok(()) } - Token::Word(w) => { - alias = Some(Ident::new(w.value)); - Ok(()) - } - _ => parser.expected("alias or file_col_num", next_token), + _ => parser.expected("file_col_num", col_num_token), }?; + } - if alias.is_some() { - parser.expect_token(&Token::Period)?; - // now we get col_num token - let col_num_token = parser.next_token(); - match col_num_token.token { - Token::Placeholder(w) => { - file_col_num = w.to_string().split_off(1).parse::().map_err(|e| { - ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}")) - })?; - Ok(()) - } - _ => parser.expected("file_col_num", col_num_token), - }?; - } - - // try extracting optional element - match parser.next_token().token { - Token::Colon => { - // parse element - element = Some(Ident::new(match parser.next_token().token { - Token::Word(w) => Ok(w.value), - _ => parser.expected("file_col_num", parser.peek_token()), - }?)); - } - _ => { - // element not present move back - parser.prev_token(); - } + // try extracting optional element + match parser.next_token().token { + Token::Colon => { + // parse element + element = Some(Ident::new(match parser.next_token().token { + Token::Word(w) => Ok(w.value), + _ => parser.expected("file_col_num", parser.peek_token()), + }?)); } - - // as - if parser.parse_keyword(Keyword::AS) { - item_as = Some(match parser.next_token().token { - Token::Word(w) => Ok(Ident::new(w.value)), - _ => parser.expected("column item alias", parser.peek_token()), - }?); + _ => { + // element not present move back + parser.prev_token(); } + } - select_items.push(StageLoadSelectItem { - alias, - file_col_num, - element, - item_as, - }); - - match parser.next_token().token { - Token::Comma => { - // continue - } - _ => { - parser.prev_token(); // need to move back - break; - } - } + // as + if parser.parse_keyword(Keyword::AS) { + item_as = Some(match parser.next_token().token { + Token::Word(w) => Ok(Ident::new(w.value)), + _ => parser.expected("column item alias", parser.peek_token()), + }?); } - Ok(Some(select_items)) + + Ok(StageLoadSelectItem { + alias, + file_col_num, + element, + item_as, + }) } fn parse_stage_params(parser: &mut Parser) -> Result { let (mut url, mut storage_integration, mut endpoint) = (None, None, None); - let mut encryption: DataLoadingOptions = DataLoadingOptions { options: vec![] }; - let mut credentials: DataLoadingOptions = DataLoadingOptions { 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) { @@ -796,16 +1431,18 @@ fn parse_stage_params(parser: &mut Parser) -> Result Result Result, ParserError> { - let mut options: Vec = Vec::new(); - - parser.expect_token(&Token::LParen)?; +/// Parses options separated by blank spaces, commas, or new lines like: +/// ABORT_DETACHED_QUERY = { TRUE | FALSE } +/// [ ACTIVE_PYTHON_PROFILER = { 'LINE' | 'MEMORY' } ] +/// [ BINARY_INPUT_FORMAT = '\' ] +fn parse_session_options( + parser: &mut Parser, + set: bool, +) -> Result, ParserError> { + let mut options: Vec = Vec::new(); + let empty = String::new; loop { - match parser.next_token().token { - Token::RParen => break, + let next_token = parser.peek_token(); + match next_token.token { + Token::SemiColon | Token::EOF => break, + Token::Comma => { + parser.advance_token(); + continue; + } Token::Word(key) => { - parser.expect_token(&Token::Eq)?; - if parser.parse_keyword(Keyword::TRUE) { - options.push(DataLoadingOption { - option_name: key.value, - option_type: DataLoadingOptionType::BOOLEAN, - value: "TRUE".to_string(), - }); - Ok(()) - } else if parser.parse_keyword(Keyword::FALSE) { - options.push(DataLoadingOption { + parser.advance_token(); + if set { + let option = parser.parse_key_value_option(&key)?; + options.push(option); + } else { + options.push(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::BOOLEAN, - value: "FALSE".to_string(), + option_value: KeyValueOptionKind::Single(Value::Placeholder(empty())), }); - Ok(()) - } else { - match parser.next_token().token { - Token::SingleQuotedString(value) => { - options.push(DataLoadingOption { - option_name: key.value, - option_type: DataLoadingOptionType::STRING, - value, - }); - Ok(()) - } - Token::Word(word) => { - options.push(DataLoadingOption { - option_name: key.value, - option_type: DataLoadingOptionType::ENUM, - value: word.value, - }); - Ok(()) - } - _ => parser.expected("expected option value", parser.peek_token()), - } } } - _ => parser.expected("another option or ')'", parser.peek_token()), - }?; + _ => { + return parser.expected("another option or end of statement", next_token); + } + } + } + if options.is_empty() { + Err(ParserError::ParserError( + "expected at least one option".to_string(), + )) + } else { + Ok(options) } - Ok(options) } /// Parsing a property of identity or autoincrement column option @@ -920,7 +1544,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())?; @@ -950,3 +1574,13 @@ fn parse_column_tags(parser: &mut Parser, with: bool) -> Result +fn parse_show_objects(terse: bool, parser: &mut Parser) -> Result { + let show_options = parser.parse_show_stmt_options()?; + Ok(Statement::ShowObjects(ShowObjects { + terse, + show_options, + })) +} 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 eb9e3ea6f..dc4ecd2f9 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -15,17 +15,17 @@ // specific language governing permissions and limitations // under the License. -//! This module defines +//! This module defines: //! 1) a list of constants for every keyword //! 2) an `ALL_KEYWORDS` array with every keyword in it -//! This is not a list of *reserved* keywords: some of these can be -//! parsed as identifiers if the parser decides so. This means that -//! new keywords can be added here without affecting the parse result. +//! This is not a list of *reserved* keywords: some of these can be +//! parsed as identifiers if the parser decides so. This means that +//! new keywords can be added here without affecting the parse result. //! -//! As a matter of fact, most of these keywords are not used at all -//! and could be removed. +//! As a matter of fact, most of these keywords are not used at all +//! and could be removed. //! 3) a `RESERVED_FOR_TABLE_ALIAS` array with keywords reserved in a -//! "table alias" context. +//! "table alias" context. #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -76,18 +76,24 @@ define_keywords!( ABS, ABSENT, ABSOLUTE, + ACCEPTANYDATE, + ACCEPTINVCHARS, ACCESS, ACCOUNT, ACTION, ADD, + ADDQUOTES, ADMIN, AFTER, AGAINST, + AGGREGATE, AGGREGATION, + ALERT, ALGORITHM, ALIAS, ALL, ALLOCATE, + ALLOWOVERWRITE, ALTER, ALWAYS, ANALYZE, @@ -96,6 +102,7 @@ define_keywords!( ANY, APPLICATION, APPLY, + APPLYBUDGET, ARCHIVE, ARE, ARRAY, @@ -109,14 +116,20 @@ define_keywords!( AT, ATOMIC, ATTACH, + AUDIT, + AUTHENTICATION, AUTHORIZATION, + AUTHORIZATIONS, AUTO, + AUTOEXTEND_SIZE, AUTOINCREMENT, AUTO_INCREMENT, AVG, + AVG_ROW_LENGTH, AVRO, BACKWARD, BASE64, + BASE_LOCATION, BEFORE, BEGIN, BEGIN_FRAME, @@ -127,14 +140,20 @@ define_keywords!( BIGINT, BIGNUMERIC, BINARY, + BIND, BINDING, BIT, + BLANKSASNULL, BLOB, BLOCK, + BLOOM, BLOOMFILTER, BOOL, BOOLEAN, + BOOST, BOTH, + BOX, + BRIN, BROWSE, BTREE, BUCKET, @@ -143,6 +162,7 @@ define_keywords!( BYPASSRLS, BYTEA, BYTES, + BZIP2, CACHE, CALL, CALLED, @@ -150,8 +170,12 @@ define_keywords!( CASCADE, CASCADED, CASE, + CASES, CAST, CATALOG, + CATALOG_SYNC, + CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER, + CATALOG_SYNC_NAMESPACE_MODE, CATCH, CEIL, CEILING, @@ -162,11 +186,15 @@ define_keywords!( CHANNEL, CHAR, CHARACTER, + CHARACTERISTICS, CHARACTERS, CHARACTER_LENGTH, CHARSET, CHAR_LENGTH, CHECK, + CHECKSUM, + CIRCLE, + CLEANPATH, CLEAR, CLOB, CLONE, @@ -185,14 +213,19 @@ define_keywords!( COMMENT, COMMIT, COMMITTED, + COMPATIBLE, COMPRESSION, + COMPUPDATE, COMPUTE, CONCURRENTLY, CONDITION, CONFLICT, CONNECT, CONNECTION, + CONNECTOR, + CONNECT_BY_ROOT, CONSTRAINT, + CONTACT, CONTAINS, CONTINUE, CONVERT, @@ -231,12 +264,14 @@ define_keywords!( DATA_RETENTION_TIME_IN_DAYS, DATE, DATE32, + DATEFORMAT, DATETIME, DATETIME64, DAY, DAYOFWEEK, DAYOFYEAR, DAYS, + DCPROPERTIES, DEALLOCATE, DEC, DECADE, @@ -244,24 +279,31 @@ define_keywords!( DECLARE, DEDUPLICATE, DEFAULT, + DEFAULTS, DEFAULT_DDL_COLLATION, + DEFAULT_MFA_METHOD, + DEFAULT_SECONDARY_ROLES, DEFERRABLE, DEFERRED, DEFINE, DEFINED, DEFINER, DELAYED, + DELAY_KEY_WRITE, + DELEGATED, DELETE, DELIMITED, DELIMITER, DELTA, DENSE_RANK, + DENY, DEREF, DESC, DESCRIBE, DETACH, DETAIL, DETERMINISTIC, + DIMENSIONS, DIRECTORY, DISABLE, DISCARD, @@ -270,21 +312,27 @@ define_keywords!( DISTRIBUTE, DIV, DO, + DOMAIN, DOUBLE, DOW, + DOWNSTREAM, DOY, DROP, DRY, + DUO, DUPLICATE, DYNAMIC, EACH, ELEMENT, ELEMENTS, ELSE, + ELSEIF, EMPTY, + EMPTYASNULL, ENABLE, ENABLE_SCHEMA_EVOLUTION, ENCODING, + ENCRYPTED, ENCRYPTION, END, END_EXEC = "END-EXEC", @@ -293,6 +341,8 @@ define_keywords!( END_PARTITION, ENFORCED, ENGINE, + ENGINE_ATTRIBUTE, + ENROLL, ENUM, ENUM16, ENUM8, @@ -305,23 +355,31 @@ define_keywords!( ESTIMATE, EVENT, EVERY, + EVOLVE, EXCEPT, EXCEPTION, + EXCHANGE, EXCLUDE, + EXCLUDING, EXCLUSIVE, EXEC, EXECUTE, + EXECUTION, EXISTS, EXP, EXPANSION, EXPLAIN, EXPLICIT, EXPORT, + EXTEND, EXTENDED, EXTENSION, EXTERNAL, + EXTERNAL_VOLUME, EXTRACT, + FACTS, FAIL, + FAILOVER, FALSE, FETCH, FIELDS, @@ -334,6 +392,8 @@ define_keywords!( FIRST, FIRST_VALUE, FIXEDSTRING, + FIXEDWIDTH, + FLATTEN, FLOAT, FLOAT32, FLOAT4, @@ -357,16 +417,21 @@ define_keywords!( FREEZE, FROM, FSCK, + FULFILLMENT, FULL, FULLTEXT, FUNCTION, FUNCTIONS, FUSION, + FUTURE, + GB, GENERAL, GENERATE, GENERATED, GEOGRAPHY, GET, + GIN, + GIST, GLOBAL, GRANT, GRANTED, @@ -375,6 +440,7 @@ define_keywords!( GROUP, GROUPING, GROUPS, + GZIP, HASH, HAVING, HEADER, @@ -386,30 +452,43 @@ define_keywords!( HOSTS, HOUR, HOURS, + HUGEINT, + IAM_ROLE, + ICEBERG, ID, IDENTITY, IDENTITY_INSERT, IF, IGNORE, + IGNOREHEADER, ILIKE, IMMEDIATE, IMMUTABLE, + IMPORT, + IMPORTED, IN, INCLUDE, INCLUDE_NULL_VALUES, + INCLUDING, INCREMENT, + INCREMENTAL, INDEX, INDICATOR, INHERIT, + INHERITS, + INITIALIZE, INITIALLY, INNER, INOUT, INPATH, + INPLACE, INPUT, INPUTFORMAT, INSENSITIVE, INSERT, + INSERT_METHOD, INSTALL, + INSTANT, INSTEAD, INT, INT128, @@ -421,11 +500,13 @@ define_keywords!( INT64, INT8, INTEGER, + INTEGRATION, INTERPOLATE, INTERSECT, INTERSECTION, INTERVAL, INTO, + INVISIBLE, INVOKER, IO, IS, @@ -443,6 +524,7 @@ define_keywords!( JULIAN, KEY, KEYS, + KEY_BLOCK_SIZE, KILL, LAG, LANGUAGE, @@ -457,9 +539,11 @@ define_keywords!( LIKE, LIKE_REGEX, LIMIT, + LINE, LINES, LIST, LISTEN, + LISTING, LN, LOAD, LOCAL, @@ -468,6 +552,7 @@ define_keywords!( LOCATION, LOCK, LOCKED, + LOG, LOGIN, LOGS, LONGBLOB, @@ -476,8 +561,12 @@ define_keywords!( LOWER, LOW_PRIORITY, LS, + LSEG, MACRO, + MANAGE, + MANAGED, MANAGEDLOCATION, + MANIFEST, MAP, MASKING, MATCH, @@ -488,16 +577,23 @@ define_keywords!( MATERIALIZE, MATERIALIZED, MAX, + MAXFILESIZE, MAXVALUE, MAX_DATA_EXTENSION_TIME_IN_DAYS, + MAX_ROWS, + MB, MEASURES, MEDIUMBLOB, MEDIUMINT, MEDIUMTEXT, MEMBER, MERGE, + MESSAGE, METADATA, METHOD, + METRIC, + METRICS, + MFA, MICROSECOND, MICROSECONDS, MILLENIUM, @@ -509,26 +605,35 @@ define_keywords!( MINUTE, MINUTES, MINVALUE, + MIN_ROWS, MOD, MODE, MODIFIES, MODIFY, MODULE, + MONITOR, MONTH, MONTHS, MSCK, MULTISET, MUTATION, NAME, + NAMES, NANOSECOND, NANOSECONDS, NATIONAL, NATURAL, NCHAR, NCLOB, + NEST, NESTED, + NETWORK, NEW, NEXT, + NFC, + NFD, + NFKC, + NFKD, NO, NOBYPASSRLS, NOCREATEDB, @@ -539,11 +644,13 @@ define_keywords!( NOORDER, NOREPLICATION, NORMALIZE, + NORMALIZED, NOSCAN, NOSUPERUSER, NOT, NOTHING, NOTIFY, + NOTNULL, NOWAIT, NO_WRITE_TO_BINLOG, NTH_VALUE, @@ -555,6 +662,7 @@ define_keywords!( NUMERIC, NVARCHAR, OBJECT, + OBJECTS, OCCURRENCES_REGEX, OCTETS, OCTET_LENGTH, @@ -567,10 +675,15 @@ define_keywords!( ON, ONE, ONLY, + ON_CREATE, + ON_SCHEDULE, OPEN, OPENJSON, + OPERATE, OPERATOR, + OPTIMIZATION, OPTIMIZE, + OPTIMIZED, OPTIMIZER_COSTS, OPTION, OPTIONS, @@ -578,23 +691,35 @@ define_keywords!( ORC, ORDER, ORDINALITY, + ORGANIZATION, + OTHER, + OTP, OUT, OUTER, + OUTPUT, OUTPUTFORMAT, OVER, OVERFLOW, OVERLAPS, OVERLAY, + OVERRIDE, OVERWRITE, OWNED, OWNER, + OWNERSHIP, + PACKAGE, + PACKAGES, + PACK_KEYS, PARALLEL, PARAMETER, PARQUET, PART, + PARTIAL, PARTITION, PARTITIONED, PARTITIONS, + PASSING, + PASSKEY, PASSWORD, PAST, PATH, @@ -611,7 +736,10 @@ define_keywords!( PLACING, PLAN, PLANS, + POINT, POLICY, + POLYGON, + POOL, PORTION, POSITION, POSITION_REGEX, @@ -622,8 +750,10 @@ define_keywords!( PRECISION, PREPARE, PRESERVE, + PRESET, PREWHERE, PRIMARY, + PRINT, PRIOR, PRIVILEGES, PROCEDURE, @@ -631,11 +761,15 @@ define_keywords!( PROGRAM, PROJECTION, PUBLIC, + PURCHASE, PURGE, QUALIFY, QUARTER, + QUERIES, QUERY, QUOTE, + RAISE, + RAISERROR, RANGE, RANK, RAW, @@ -649,8 +783,11 @@ define_keywords!( REF, REFERENCES, REFERENCING, + REFRESH, + REFRESH_MODE, REGCLASS, REGEXP, + REGION, REGR_AVGX, REGR_AVGY, REGR_COUNT, @@ -660,23 +797,32 @@ define_keywords!( REGR_SXX, REGR_SXY, REGR_SYY, + REINDEX, RELATIVE, RELAY, RELEASE, + RELEASES, REMOTE, REMOVE, + REMOVEQUOTES, RENAME, REORG, REPAIR, REPEATABLE, REPLACE, + REPLACE_INVALID_CHARACTERS, REPLICA, + REPLICATE, REPLICATION, + REQUIRE, RESET, + RESOLVE, + RESOURCE, RESPECT, RESTART, RESTRICT, RESTRICTED, + RESTRICTIONS, RESTRICTIVE, RESULT, RESULTSET, @@ -695,8 +841,10 @@ define_keywords!( ROLLUP, ROOT, ROW, + ROWGROUPSIZE, ROWID, ROWS, + ROW_FORMAT, ROW_NUMBER, RULE, RUN, @@ -711,11 +859,14 @@ define_keywords!( SEARCH, SECOND, SECONDARY, + SECONDARY_ENGINE_ATTRIBUTE, SECONDS, SECRET, + SECURE, SECURITY, SEED, SELECT, + SEMANTIC_VIEW, SEMI, SENSITIVE, SEPARATOR, @@ -725,14 +876,21 @@ define_keywords!( SERDE, SERDEPROPERTIES, SERIALIZABLE, + SERVER, + SERVICE, SESSION, SESSION_USER, SET, + SETERROR, SETS, SETTINGS, SHARE, + SHARED, + SHARING, SHOW, + SIGNED, SIMILAR, + SIMPLE, SKIP, SLOW, SMALLINT, @@ -744,11 +902,13 @@ define_keywords!( SPATIAL, SPECIFIC, SPECIFICTYPE, + SPGIST, SQL, SQLEXCEPTION, SQLSTATE, SQLWARNING, SQRT, + SRID, STABLE, STAGE, START, @@ -756,24 +916,34 @@ define_keywords!( STATEMENT, STATIC, STATISTICS, + STATS_AUTO_RECALC, + STATS_PERSISTENT, + STATS_SAMPLE_PAGES, + STATUPDATE, STATUS, STDDEV_POP, STDDEV_SAMP, STDIN, STDOUT, STEP, + STORAGE, STORAGE_INTEGRATION, + STORAGE_SERIALIZATION_POLICY, STORED, + STRAIGHT_JOIN, + STREAM, STRICT, STRING, STRUCT, SUBMULTISET, + SUBSTR, SUBSTRING, SUBSTRING_REGEX, SUCCEEDS, SUM, SUPER, SUPERUSER, + SUPPORT, SUSPEND, SWAP, SYMMETRIC, @@ -784,8 +954,11 @@ define_keywords!( TABLE, TABLES, TABLESAMPLE, + TABLESPACE, TAG, TARGET, + TARGET_LAG, + TASK, TBLPROPERTIES, TEMP, TEMPORARY, @@ -797,8 +970,10 @@ define_keywords!( THEN, TIES, TIME, + TIMEFORMAT, TIMESTAMP, TIMESTAMPTZ, + TIMESTAMP_NTZ, TIMETZ, TIMEZONE, TIMEZONE_ABBR, @@ -811,6 +986,8 @@ define_keywords!( TO, TOP, TOTALS, + TOTP, + TRACE, TRAILING, TRANSACTION, TRANSIENT, @@ -823,12 +1000,17 @@ define_keywords!( TRIM_ARRAY, TRUE, TRUNCATE, + TRUNCATECOLUMNS, TRY, TRY_CAST, TRY_CONVERT, + TSQUERY, + TSVECTOR, TUPLE, TYPE, + UBIGINT, UESCAPE, + UHUGEINT, UINT128, UINT16, UINT256, @@ -851,6 +1033,7 @@ define_keywords!( UNNEST, UNPIVOT, UNSAFE, + UNSET, UNSIGNED, UNTIL, UPDATE, @@ -861,14 +1044,18 @@ define_keywords!( USER, USER_RESOURCES, USING, + USMALLINT, + UTINYINT, UUID, VACUUM, VALID, + VALIDATE, VALIDATION_MODE, VALUE, VALUES, VALUE_OF, VARBINARY, + VARBIT, VARCHAR, VARIABLES, VARYING, @@ -877,16 +1064,20 @@ define_keywords!( VERBOSE, VERSION, VERSIONING, + VERSIONS, VIEW, VIEWS, VIRTUAL, VOLATILE, + VOLUME, WAREHOUSE, + WAREHOUSES, WEEK, WEEKS, WHEN, WHENEVER, WHERE, + WHILE, WIDTH_BUCKET, WINDOW, WITH, @@ -894,13 +1085,18 @@ define_keywords!( WITHOUT, WITHOUT_ARRAY_WRAPPER, WORK, + WORKLOAD_IDENTITY, + WRAPPER, WRITE, XML, + XMLNAMESPACES, + XMLTABLE, XOR, YEAR, YEARS, ZONE, - ZORDER + ZORDER, + ZSTD ); /// These keywords can't be used as a table alias, so that `FROM table_name alias` @@ -943,6 +1139,9 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::GLOBAL, Keyword::ANTI, Keyword::SEMI, + Keyword::RETURNING, + Keyword::ASOF, + Keyword::MATCH_CONDITION, // for MSSQL-specific OUTER APPLY (seems reserved in most dialects) Keyword::OUTER, Keyword::SET, @@ -965,6 +1164,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` @@ -988,6 +1188,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::FETCH, Keyword::UNION, Keyword::EXCEPT, + Keyword::EXCLUDE, Keyword::INTERSECT, Keyword::MINUS, Keyword::CLUSTER, @@ -999,7 +1200,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::END, ]; -// Global list of reserved keywords alloweed after FROM. +// Global list of reserved keywords allowed after FROM. // Parser should call Dialect::get_reserved_keyword_after_from // to allow for each dialect to customize the list. pub const RESERVED_FOR_TABLE_FACTOR: &[Keyword] = &[ 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/alter.rs b/src/parser/alter.rs index bb6782c13..b3e3c99e6 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -13,13 +13,16 @@ //! SQL Parser for ALTER #[cfg(not(feature = "std"))] -use alloc::vec; +use alloc::{string::ToString, vec}; use super::{Parser, ParserError}; use crate::{ ast::{ - AlterPolicyOperation, AlterRoleOperation, Expr, Password, ResetConfig, RoleOption, - SetConfigValue, Statement, + helpers::key_value_options::{KeyValueOptions, KeyValueOptionsDelimiter}, + AlterConnectorOwner, AlterPolicyOperation, AlterRoleOperation, AlterUser, + AlterUserAddMfaMethodOtp, AlterUserAddRoleDelegation, AlterUserModifyMfaMethod, + AlterUserRemoveRoleDelegation, AlterUserSetPolicy, Expr, MfaMethodKind, Password, + ResetConfig, RoleOption, SetConfigValue, Statement, UserPolicyKind, }, dialect::{MsSqlDialect, PostgreSqlDialect}, keywords::Keyword, @@ -99,6 +102,230 @@ impl Parser<'_> { } } + /// Parse an `ALTER CONNECTOR` statement + /// ```sql + /// ALTER CONNECTOR connector_name SET DCPROPERTIES(property_name=property_value, ...); + /// + /// ALTER CONNECTOR connector_name SET URL new_url; + /// + /// ALTER CONNECTOR connector_name SET OWNER [USER|ROLE] user_or_role; + /// ``` + pub fn parse_alter_connector(&mut self) -> Result { + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::SET)?; + + let properties = match self.parse_options_with_keywords(&[Keyword::DCPROPERTIES])? { + properties if !properties.is_empty() => Some(properties), + _ => None, + }; + + let url = if self.parse_keyword(Keyword::URL) { + Some(self.parse_literal_string()?) + } else { + None + }; + + let owner = if self.parse_keywords(&[Keyword::OWNER, Keyword::USER]) { + let owner = self.parse_identifier()?; + Some(AlterConnectorOwner::User(owner)) + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::ROLE]) { + let owner = self.parse_identifier()?; + Some(AlterConnectorOwner::Role(owner)) + } else { + None + }; + + Ok(Statement::AlterConnector { + name, + properties, + url, + owner, + }) + } + + /// Parse an `ALTER USER` statement + /// ```sql + /// ALTER USER [ IF EXISTS ] [ ] [ OPTIONS ] + /// ``` + pub fn parse_alter_user(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier()?; + let rename_to = if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + Some(self.parse_identifier()?) + } else { + None + }; + let reset_password = self.parse_keywords(&[Keyword::RESET, Keyword::PASSWORD]); + let abort_all_queries = + self.parse_keywords(&[Keyword::ABORT, Keyword::ALL, Keyword::QUERIES]); + let add_role_delegation = if self.parse_keywords(&[ + Keyword::ADD, + Keyword::DELEGATED, + Keyword::AUTHORIZATION, + Keyword::OF, + Keyword::ROLE, + ]) { + let role = self.parse_identifier()?; + self.expect_keywords(&[Keyword::TO, Keyword::SECURITY, Keyword::INTEGRATION])?; + let integration = self.parse_identifier()?; + Some(AlterUserAddRoleDelegation { role, integration }) + } else { + None + }; + let remove_role_delegation = if self.parse_keywords(&[Keyword::REMOVE, Keyword::DELEGATED]) + { + let role = if self.parse_keywords(&[Keyword::AUTHORIZATION, Keyword::OF, Keyword::ROLE]) + { + Some(self.parse_identifier()?) + } else if self.parse_keyword(Keyword::AUTHORIZATIONS) { + None + } else { + return self.expected( + "REMOVE DELEGATED AUTHORIZATION OF ROLE | REMOVE DELEGATED AUTHORIZATIONS", + self.peek_token(), + ); + }; + self.expect_keywords(&[Keyword::FROM, Keyword::SECURITY, Keyword::INTEGRATION])?; + let integration = self.parse_identifier()?; + Some(AlterUserRemoveRoleDelegation { role, integration }) + } else { + None + }; + let enroll_mfa = self.parse_keywords(&[Keyword::ENROLL, Keyword::MFA]); + let set_default_mfa_method = + if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT_MFA_METHOD]) { + Some(self.parse_mfa_method()?) + } else { + None + }; + let remove_mfa_method = + if self.parse_keywords(&[Keyword::REMOVE, Keyword::MFA, Keyword::METHOD]) { + Some(self.parse_mfa_method()?) + } else { + None + }; + let modify_mfa_method = + if self.parse_keywords(&[Keyword::MODIFY, Keyword::MFA, Keyword::METHOD]) { + let method = self.parse_mfa_method()?; + self.expect_keywords(&[Keyword::SET, Keyword::COMMENT])?; + let comment = self.parse_literal_string()?; + Some(AlterUserModifyMfaMethod { method, comment }) + } else { + None + }; + let add_mfa_method_otp = + if self.parse_keywords(&[Keyword::ADD, Keyword::MFA, Keyword::METHOD, Keyword::OTP]) { + let count = if self.parse_keyword(Keyword::COUNT) { + self.expect_token(&Token::Eq)?; + Some(self.parse_value()?.into()) + } else { + None + }; + Some(AlterUserAddMfaMethodOtp { count }) + } else { + None + }; + let set_policy = + if self.parse_keywords(&[Keyword::SET, Keyword::AUTHENTICATION, Keyword::POLICY]) { + Some(AlterUserSetPolicy { + policy_kind: UserPolicyKind::Authentication, + policy: self.parse_identifier()?, + }) + } else if self.parse_keywords(&[Keyword::SET, Keyword::PASSWORD, Keyword::POLICY]) { + Some(AlterUserSetPolicy { + policy_kind: UserPolicyKind::Password, + policy: self.parse_identifier()?, + }) + } else if self.parse_keywords(&[Keyword::SET, Keyword::SESSION, Keyword::POLICY]) { + Some(AlterUserSetPolicy { + policy_kind: UserPolicyKind::Session, + policy: self.parse_identifier()?, + }) + } else { + None + }; + + let unset_policy = + if self.parse_keywords(&[Keyword::UNSET, Keyword::AUTHENTICATION, Keyword::POLICY]) { + Some(UserPolicyKind::Authentication) + } else if self.parse_keywords(&[Keyword::UNSET, Keyword::PASSWORD, Keyword::POLICY]) { + Some(UserPolicyKind::Password) + } else if self.parse_keywords(&[Keyword::UNSET, Keyword::SESSION, Keyword::POLICY]) { + Some(UserPolicyKind::Session) + } else { + None + }; + + let set_tag = if self.parse_keywords(&[Keyword::SET, Keyword::TAG]) { + self.parse_key_value_options(false, &[])? + } else { + KeyValueOptions { + delimiter: KeyValueOptionsDelimiter::Comma, + options: vec![], + } + }; + + let unset_tag = if self.parse_keywords(&[Keyword::UNSET, Keyword::TAG]) { + self.parse_comma_separated(Parser::parse_identifier)? + .iter() + .map(|i| i.to_string()) + .collect() + } else { + vec![] + }; + + let set_props = if self.parse_keyword(Keyword::SET) { + self.parse_key_value_options(false, &[])? + } else { + KeyValueOptions { + delimiter: KeyValueOptionsDelimiter::Comma, + options: vec![], + } + }; + + let unset_props = if self.parse_keyword(Keyword::UNSET) { + self.parse_comma_separated(Parser::parse_identifier)? + .iter() + .map(|i| i.to_string()) + .collect() + } else { + vec![] + }; + + Ok(Statement::AlterUser(AlterUser { + if_exists, + name, + rename_to, + reset_password, + abort_all_queries, + add_role_delegation, + remove_role_delegation, + enroll_mfa, + set_default_mfa_method, + remove_mfa_method, + modify_mfa_method, + add_mfa_method_otp, + set_policy, + unset_policy, + set_tag, + unset_tag, + set_props, + unset_props, + })) + } + + fn parse_mfa_method(&mut self) -> Result { + if self.parse_keyword(Keyword::PASSKEY) { + Ok(MfaMethodKind::PassKey) + } else if self.parse_keyword(Keyword::TOTP) { + Ok(MfaMethodKind::Totp) + } else if self.parse_keyword(Keyword::DUO) { + Ok(MfaMethodKind::Duo) + } else { + self.expected("PASSKEY, TOTP or DUO", self.peek_token()) + } + } + fn parse_mssql_alter_role(&mut self) -> Result { let role_name = self.parse_identifier()?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ac764a535..e4a5af72a 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, KeyValueOptionKind, 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; @@ -48,9 +54,6 @@ pub enum ParserError { RecursionLimitExceeded, } -// avoid clippy type_complexity warnings -type ParsedAction = (Keyword, Option>); - // Use `Parser::expected` instead, if possible macro_rules! parser_err { ($MSG:expr, $loc:expr) => { @@ -225,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 { @@ -232,6 +238,7 @@ impl Default for ParserOptions { Self { trailing_commas: false, unescape: true, + require_semicolon_stmt_delimiter: true, } } } @@ -274,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 @@ -439,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()?; @@ -470,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, @@ -531,7 +548,23 @@ impl<'a> Parser<'a> { Keyword::DESCRIBE => self.parse_explain(DescribeAlias::Describe), Keyword::EXPLAIN => self.parse_explain(DescribeAlias::Explain), Keyword::ANALYZE => self.parse_analyze(), - Keyword::SELECT | Keyword::WITH | Keyword::VALUES => { + Keyword::CASE => { + self.prev_token(); + self.parse_case_stmt() + } + Keyword::IF => { + self.prev_token(); + self.parse_if_stmt() + } + Keyword::WHILE => { + self.prev_token(); + self.parse_while() + } + Keyword::RAISE => { + self.prev_token(); + self.parse_raise_stmt() + } + Keyword::SELECT | Keyword::WITH | Keyword::VALUES | Keyword::FROM => { self.prev_token(); self.parse_query().map(Statement::Query) } @@ -561,24 +594,27 @@ 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(), - // `BEGIN` is a nonstandard but common alias for the - // standard `START TRANSACTION` statement. It is supported - // by at least PostgreSQL and MySQL. Keyword::BEGIN => self.parse_begin(), - // `END` is a nonstandard but common alias for the - // standard `COMMIT TRANSACTION` statement. It is supported - // by PostgreSQL. Keyword::END => self.parse_end(), Keyword::SAVEPOINT => self.parse_savepoint(), Keyword::RELEASE => self.parse_release(), Keyword::COMMIT => self.parse_commit(), + Keyword::RAISERROR => Ok(self.parse_raiserror()?), Keyword::ROLLBACK => self.parse_rollback(), Keyword::ASSERT => self.parse_assert(), // `PREPARE`, `EXECUTE` and `DEALLOCATE` are Postgres-specific @@ -594,7 +630,10 @@ impl<'a> Parser<'a> { Keyword::NOTIFY if self.dialect.supports_listen_notify() => self.parse_notify(), // `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html Keyword::PRAGMA => self.parse_pragma(), - Keyword::UNLOAD => self.parse_unload(), + Keyword::UNLOAD => { + self.prev_token(); + self.parse_unload() + } Keyword::RENAME => self.parse_rename(), // `INSTALL` is duckdb specific https://duckdb.org/docs/extensions/overview Keyword::INSTALL if dialect_of!(self is DuckDbDialect | GenericDialect) => { @@ -607,6 +646,17 @@ impl<'a> Parser<'a> { } // `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment 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() + } + Keyword::RESET => self.parse_reset(), _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -617,6 +667,170 @@ impl<'a> Parser<'a> { } } + /// Parse a `CASE` statement. + /// + /// See [Statement::Case] + pub fn parse_case_stmt(&mut self) -> Result { + let case_token = self.expect_keyword(Keyword::CASE)?; + + let match_expr = if self.peek_keyword(Keyword::WHEN) { + None + } else { + Some(self.parse_expr()?) + }; + + self.expect_keyword_is(Keyword::WHEN)?; + let when_blocks = self.parse_keyword_separated(Keyword::WHEN, |parser| { + parser.parse_conditional_statement_block(&[Keyword::WHEN, Keyword::ELSE, Keyword::END]) + })?; + + let else_block = if self.parse_keyword(Keyword::ELSE) { + Some(self.parse_conditional_statement_block(&[Keyword::END])?) + } else { + None + }; + + let mut end_case_token = self.expect_keyword(Keyword::END)?; + if self.peek_keyword(Keyword::CASE) { + end_case_token = self.expect_keyword(Keyword::CASE)?; + } + + Ok(Statement::Case(CaseStatement { + case_token: AttachedToken(case_token), + match_expr, + when_blocks, + else_block, + end_case_token: AttachedToken(end_case_token), + })) + } + + /// Parse an `IF` statement. + /// + /// See [Statement::If] + pub fn parse_if_stmt(&mut self) -> Result { + self.expect_keyword_is(Keyword::IF)?; + let if_block = self.parse_conditional_statement_block(&[ + Keyword::ELSE, + Keyword::ELSEIF, + Keyword::END, + ])?; + + let elseif_blocks = if self.parse_keyword(Keyword::ELSEIF) { + self.parse_keyword_separated(Keyword::ELSEIF, |parser| { + parser.parse_conditional_statement_block(&[ + Keyword::ELSEIF, + Keyword::ELSE, + Keyword::END, + ]) + })? + } else { + vec![] + }; + + let else_block = if self.parse_keyword(Keyword::ELSE) { + Some(self.parse_conditional_statement_block(&[Keyword::END])?) + } else { + None + }; + + self.expect_keyword_is(Keyword::END)?; + let end_token = self.expect_keyword(Keyword::IF)?; + + Ok(Statement::If(IfStatement { + if_block, + elseif_blocks, + else_block, + end_token: Some(AttachedToken(end_token)), + })) + } + + /// 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` or `WHILE`. + /// + /// Example: + /// ```sql + /// IF condition THEN statement1; statement2; + /// ``` + fn parse_conditional_statement_block( + &mut self, + terminal_keywords: &[Keyword], + ) -> Result { + let start_token = self.get_current_token().clone(); // self.expect_keyword(keyword)?; + let mut then_token = None; + + 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)?)); + Some(expr) + } + }; + + let conditional_statements = self.parse_conditional_statements(terminal_keywords)?; + + Ok(ConditionalStatementBlock { + start_token: AttachedToken(start_token), + condition, + then_token, + 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] + pub fn parse_raise_stmt(&mut self) -> Result { + self.expect_keyword_is(Keyword::RAISE)?; + + let value = if self.parse_keywords(&[Keyword::USING, Keyword::MESSAGE]) { + self.expect_token(&Token::Eq)?; + Some(RaiseStatementValue::UsingMessage(self.parse_expr()?)) + } else { + self.maybe_parse(|parser| parser.parse_expr().map(RaiseStatementValue::Expr))? + }; + + Ok(Statement::Raise(RaiseStatement { value })) + } + pub fn parse_comment(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); @@ -769,21 +983,23 @@ impl<'a> Parser<'a> { Ok(pa) })? .unwrap_or_default(); - Ok(Statement::Msck { + Ok(Msck { repair, table_name, partition_action, - }) + } + .into()) } 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; @@ -810,15 +1026,15 @@ impl<'a> Parser<'a> { let on_cluster = self.parse_optional_on_cluster()?; - Ok(Statement::Truncate { + Ok(Truncate { table_names, partitions, table, - only, identity, cascade, on_cluster, - }) + } + .into()) } fn parse_cascade_option(&mut self) -> Option { @@ -954,7 +1170,7 @@ impl<'a> Parser<'a> { } } - Ok(Statement::Analyze { + Ok(Analyze { has_table_keyword, table_name, for_columns, @@ -963,7 +1179,8 @@ impl<'a> Parser<'a> { cache_metadata, noscan, compute_statistics, - }) + } + .into()) } /// Parse a new expression including wildcard & qualified wildcard. @@ -990,7 +1207,7 @@ impl<'a> Parser<'a> { } Token::Mul => { return Ok(Expr::QualifiedWildcard( - ObjectName(id_parts), + ObjectName::from(id_parts), AttachedToken(next_token), )); } @@ -1017,21 +1234,48 @@ 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()?; debug!("parsing expr"); let mut expr = self.parse_prefix()?; - debug!("prefix: {:?}", expr); + expr = self.parse_compound_expr(expr, vec![])?; + + 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; } + // The period operator is handled exclusively by the + // compound field access parsing. + if Token::Period == self.peek_token_ref().token { + break; + } + expr = self.parse_infix(expr, next_precedence)?; } Ok(expr) @@ -1107,8 +1351,8 @@ impl<'a> Parser<'a> { } } - // Tries to parse an expression by matching the specified word to known keywords that have a special meaning in the dialect. - // Returns `None if no match is found. + /// Tries to parse an expression by matching the specified word to known keywords that have a special meaning in the dialect. + /// Returns `None if no match is found. fn parse_expr_prefix_by_reserved_word( &mut self, w: &Word, @@ -1130,7 +1374,7 @@ impl<'a> Parser<'a> { if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.clone().into_ident(w_span)]), + name: ObjectName::from(vec![w.clone().into_ident(w_span)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -1145,7 +1389,7 @@ impl<'a> Parser<'a> { | Keyword::CURRENT_DATE | Keyword::LOCALTIME | Keyword::LOCALTIMESTAMP => { - Ok(Some(self.parse_time_functions(ObjectName(vec![w.clone().into_ident(w_span)]))?)) + Ok(Some(self.parse_time_functions(ObjectName::from(vec![w.clone().into_ident(w_span)]))?)) } Keyword::CASE => Ok(Some(self.parse_case_expr()?)), Keyword::CONVERT => Ok(Some(self.parse_convert_expr(false)?)), @@ -1172,7 +1416,10 @@ impl<'a> Parser<'a> { Keyword::POSITION if self.peek_token_ref().token == Token::LParen => { Ok(Some(self.parse_position_expr(w.clone().into_ident(w_span))?)) } - Keyword::SUBSTRING => Ok(Some(self.parse_substring_expr()?)), + Keyword::SUBSTR | Keyword::SUBSTRING => { + self.prev_token(); + Ok(Some(self.parse_substring()?)) + } Keyword::OVERLAY => Ok(Some(self.parse_overlay_expr()?)), Keyword::TRIM => Ok(Some(self.parse_trim_expr()?)), Keyword::INTERVAL => Ok(Some(self.parse_interval()?)), @@ -1189,7 +1436,7 @@ impl<'a> Parser<'a> { let query = self.parse_query()?; self.expect_token(&Token::RParen)?; Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.clone().into_ident(w_span)]), + name: ObjectName::from(vec![w.clone().into_ident(w_span)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::Subquery(query), @@ -1200,12 +1447,12 @@ impl<'a> Parser<'a> { }))) } Keyword::NOT => Ok(Some(self.parse_not()?)), - Keyword::MATCH if dialect_of!(self is MySqlDialect | GenericDialect) => { + Keyword::MATCH if self.dialect.supports_match_against() => { Ok(Some(self.parse_match_against()?)) } Keyword::STRUCT if self.dialect.supports_struct_literal() => { let struct_expr = self.parse_struct_literal()?; - Ok(Some(self.parse_compound_field_access(struct_expr, vec![])?)) + Ok(Some(struct_expr)) } Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => { let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?; @@ -1214,39 +1461,30 @@ impl<'a> Parser<'a> { Keyword::MAP if *self.peek_token_ref() == Token::LBrace && self.dialect.support_map_literal_syntax() => { Ok(Some(self.parse_duckdb_map_literal()?)) } - _ => Ok(None) + _ if self.dialect.supports_geometric_types() => match w.keyword { + Keyword::CIRCLE => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Circle)?)), + Keyword::BOX => Ok(Some(self.parse_geometric_type(GeometricTypeKind::GeometricBox)?)), + Keyword::PATH => Ok(Some(self.parse_geometric_type(GeometricTypeKind::GeometricPath)?)), + Keyword::LINE => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Line)?)), + Keyword::LSEG => Ok(Some(self.parse_geometric_type(GeometricTypeKind::LineSegment)?)), + Keyword::POINT => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Point)?)), + Keyword::POLYGON => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Polygon)?)), + _ => Ok(None), + }, + _ => Ok(None), } } - // Tries to parse an expression by a word that is not known to have a special meaning in the dialect. + /// Tries to parse an expression by a word that is not known to have a special meaning in the dialect. fn parse_expr_prefix_by_unreserved_word( &mut self, w: &Word, w_span: Span, ) -> Result { match self.peek_token().token { - Token::Period => self.parse_compound_field_access( - Expr::Identifier(w.clone().into_ident(w_span)), - vec![], - ), - Token::LParen => { + Token::LParen if !self.peek_outer_join_operator() => { let id_parts = vec![w.clone().into_ident(w_span)]; - if let Some(expr) = self.parse_outer_join_expr(&id_parts) { - Ok(expr) - } else { - let mut expr = self.parse_function(ObjectName(id_parts))?; - // consume all period if it's a method chain - expr = self.try_parse_method(expr)?; - let fields = vec![]; - self.parse_compound_field_access(expr, fields) - } - } - Token::LBracket if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect) => - { - let ident = Expr::Identifier(w.clone().into_ident(w_span)); - let mut fields = vec![]; - self.parse_multi_dim_subscript(&mut fields)?; - self.parse_compound_field_access(ident, fields) + self.parse_function(ObjectName::from(id_parts)) } // string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html Token::SingleQuotedString(_) @@ -1254,9 +1492,9 @@ impl<'a> Parser<'a> { | Token::HexStringLiteral(_) if w.value.starts_with('_') => { - Ok(Expr::IntroducedString { - introducer: w.value.clone(), - value: self.parse_introduced_string_value()?, + Ok(Expr::Prefixed { + prefix: w.clone().into_ident(w_span), + value: self.parse_introduced_string_expr()?.into(), }) } // string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html @@ -1265,9 +1503,9 @@ impl<'a> Parser<'a> { | Token::HexStringLiteral(_) if w.value.starts_with('_') => { - Ok(Expr::IntroducedString { - introducer: w.value.clone(), - value: self.parse_introduced_string_value()?, + Ok(Expr::Prefixed { + prefix: w.clone().into_ident(w_span), + value: self.parse_introduced_string_expr()?.into(), }) } Token::Arrow if self.dialect.supports_lambda_functions() => { @@ -1307,7 +1545,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), @@ -1316,10 +1554,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_literal_string()?, - }), + value: parser.parse_value()?, + uses_odbc_syntax: false, + })), } })?; @@ -1395,7 +1634,6 @@ impl<'a> Parser<'a> { | tok @ Token::PGSquareRoot | tok @ Token::PGCubeRoot | tok @ Token::AtSign - | tok @ Token::Tilde if dialect_is!(dialect is PostgreSqlDialect) => { let op = match tok { @@ -1403,7 +1641,6 @@ impl<'a> Parser<'a> { Token::PGSquareRoot => UnaryOperator::PGSquareRoot, Token::PGCubeRoot => UnaryOperator::PGCubeRoot, Token::AtSign => UnaryOperator::PGAbs, - Token::Tilde => UnaryOperator::PGBitwiseNot, _ => unreachable!(), }; Ok(Expr::UnaryOp { @@ -1413,6 +1650,36 @@ impl<'a> Parser<'a> { ), }) } + Token::Tilde => Ok(Expr::UnaryOp { + op: UnaryOperator::BitwiseNot, + expr: Box::new(self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?), + }), + tok @ Token::Sharp + | tok @ Token::AtDashAt + | tok @ Token::AtAt + | tok @ Token::QuestionMarkDash + | tok @ Token::QuestionPipe + if self.dialect.supports_geometric_types() => + { + let op = match tok { + Token::Sharp => UnaryOperator::Hash, + Token::AtDashAt => UnaryOperator::AtDashAt, + Token::AtAt => UnaryOperator::DoubleAt, + Token::QuestionMarkDash => UnaryOperator::QuestionDash, + Token::QuestionPipe => UnaryOperator::QuestionPipe, + _ => { + return Err(ParserError::ParserError(format!( + "Unexpected token in unary operator parsing: {tok:?}" + ))) + } + }; + Ok(Expr::UnaryOp { + op, + expr: Box::new( + self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?, + ), + }) + } Token::EscapedStringLiteral(_) if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { self.prev_token(); @@ -1455,25 +1722,7 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::RParen)?; - let expr = self.try_parse_method(expr)?; - if !self.consume_token(&Token::Period) { - Ok(expr) - } else { - let tok = self.next_token(); - let key = match tok.token { - Token::Word(word) => word.into_ident(tok.span), - _ => { - return parser_err!( - format!("Expected identifier, found: {tok}"), - tok.span.start - ) - } - }; - Ok(Expr::CompositeAccess { - expr: Box::new(expr), - key, - }) - } + Ok(expr) } Token::Placeholder(_) | Token::Colon | Token::AtSign => { self.prev_token(); @@ -1486,9 +1735,7 @@ impl<'a> Parser<'a> { _ => self.expected_at("an expression", next_token_index), }?; - let expr = self.try_parse_method(expr)?; - - 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)?, @@ -1498,91 +1745,205 @@ impl<'a> Parser<'a> { } } + fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result { + Ok(Expr::TypedString(TypedString { + data_type: DataType::GeometricType(kind), + value: self.parse_value()?, + uses_odbc_syntax: false, + })) + } + /// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`. /// If all the fields are `Expr::Identifier`s, return an [Expr::CompoundIdentifier] instead. /// If only the root exists, return the root. - /// If self supports [Dialect::supports_partiql], it will fall back when occurs [Token::LBracket] for JsonAccess parsing. - pub fn parse_compound_field_access( + /// Parses compound expressions which may be delimited by period + /// or bracket notation. + /// For example: `a.b.c`, `a.b[1]`. + pub fn parse_compound_expr( &mut self, root: Expr, mut chain: Vec, ) -> Result { let mut ending_wildcard: Option = None; - let mut ending_lbracket = false; - while self.consume_token(&Token::Period) { - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => { - let expr = Expr::Identifier(w.into_ident(next_token.span)); - chain.push(AccessExpr::Dot(expr)); - if self.peek_token().token == Token::LBracket { - if self.dialect.supports_partiql() { - self.next_token(); - ending_lbracket = true; - break; + loop { + if self.consume_token(&Token::Period) { + let next_token = self.peek_token_ref(); + match &next_token.token { + Token::Mul => { + // Postgres explicitly allows funcnm(tablenm.*) and the + // function array_agg traverses this control flow + if dialect_of!(self is PostgreSqlDialect) { + ending_wildcard = Some(self.next_token()); } else { - self.parse_multi_dim_subscript(&mut chain)? + // Put back the consumed `.` tokens before exiting. + // If this expression is being parsed in the + // context of a projection, then the `.*` could imply + // a wildcard expansion. For example: + // `SELECT STRUCT('foo').* FROM T` + self.prev_token(); // . } - } - } - Token::Mul => { - // Postgres explicitly allows funcnm(tablenm.*) and the - // function array_agg traverses this control flow - if dialect_of!(self is PostgreSqlDialect) { - ending_wildcard = Some(next_token); + break; - } else { - return self.expected("an identifier after '.'", next_token); } + Token::SingleQuotedString(s) => { + let expr = + Expr::Identifier(Ident::with_quote_and_span('\'', next_token.span, s)); + chain.push(AccessExpr::Dot(expr)); + self.advance_token(); // The consumed string + } + // Fallback to parsing an arbitrary expression. + _ => match self.parse_subexpr(self.dialect.prec_value(Precedence::Period))? { + // If we get back a compound field access or identifier, + // we flatten the nested expression. + // For example if the current root is `foo` + // and we get back a compound identifier expression `bar.baz` + // The full expression should be `foo.bar.baz` (i.e. + // a root with an access chain with 2 entries) and not + // `foo.(bar.baz)` (i.e. a root with an access chain with + // 1 entry`). + Expr::CompoundFieldAccess { root, access_chain } => { + chain.push(AccessExpr::Dot(*root)); + chain.extend(access_chain); + } + Expr::CompoundIdentifier(parts) => chain + .extend(parts.into_iter().map(Expr::Identifier).map(AccessExpr::Dot)), + expr => { + chain.push(AccessExpr::Dot(expr)); + } + }, } - Token::SingleQuotedString(s) => { - let expr = Expr::Identifier(Ident::with_quote('\'', s)); - chain.push(AccessExpr::Dot(expr)); - } - _ => { - return self.expected("an identifier or a '*' after '.'", next_token); - } + } else if !self.dialect.supports_partiql() + && self.peek_token_ref().token == Token::LBracket + { + self.parse_multi_dim_subscript(&mut chain)?; + } else { + break; } } - // if dialect supports partiql, we need to go back one Token::LBracket for the JsonAccess parsing - if self.dialect.supports_partiql() && ending_lbracket { - self.prev_token(); - } - + let tok_index = self.get_current_index(); if let Some(wildcard_token) = ending_wildcard { if !Self::is_all_ident(&root, &chain) { return self.expected("an identifier or a '*' after '.'", self.peek_token()); }; Ok(Expr::QualifiedWildcard( - ObjectName(Self::exprs_to_idents(root, chain)?), + ObjectName::from(Self::exprs_to_idents(root, chain)?), AttachedToken(wildcard_token), )) - } else if self.peek_token().token == Token::LParen { + } else if self.maybe_parse_outer_join_operator() { if !Self::is_all_ident(&root, &chain) { - // consume LParen - self.next_token(); - return self.expected("an identifier or a '*' after '.'", self.peek_token()); + return self.expected_at("column identifier before (+)", tok_index); }; - let id_parts = Self::exprs_to_idents(root, chain)?; - if let Some(expr) = self.parse_outer_join_expr(&id_parts) { - Ok(expr) + let expr = if chain.is_empty() { + root } else { - self.parse_function(ObjectName(id_parts)) - } + Expr::CompoundIdentifier(Self::exprs_to_idents(root, chain)?) + }; + Ok(Expr::OuterJoin(expr.into())) } else { - if Self::is_all_ident(&root, &chain) { - return Ok(Expr::CompoundIdentifier(Self::exprs_to_idents( - root, chain, - )?)); - } - if chain.is_empty() { - return Ok(root); + Self::build_compound_expr(root, chain) + } + } + + /// Combines a root expression and access chain to form + /// a compound expression. Which may be a [Expr::CompoundFieldAccess] + /// or other special cased expressions like [Expr::CompoundIdentifier], + /// [Expr::OuterJoin]. + fn build_compound_expr( + root: Expr, + mut access_chain: Vec, + ) -> Result { + if access_chain.is_empty() { + return Ok(root); + } + + if Self::is_all_ident(&root, &access_chain) { + return Ok(Expr::CompoundIdentifier(Self::exprs_to_idents( + root, + access_chain, + )?)); + } + + // Flatten qualified function calls. + // For example, the expression `a.b.c.foo(1,2,3)` should + // represent a function called `a.b.c.foo`, rather than + // a composite expression. + if matches!(root, Expr::Identifier(_)) + && matches!( + access_chain.last(), + Some(AccessExpr::Dot(Expr::Function(_))) + ) + && access_chain + .iter() + .rev() + .skip(1) // All except the Function + .all(|access| matches!(access, AccessExpr::Dot(Expr::Identifier(_)))) + { + let Some(AccessExpr::Dot(Expr::Function(mut func))) = access_chain.pop() else { + return parser_err!("expected function expression", root.span().start); + }; + + let compound_func_name = [root] + .into_iter() + .chain(access_chain.into_iter().flat_map(|access| match access { + AccessExpr::Dot(expr) => Some(expr), + _ => None, + })) + .flat_map(|expr| match expr { + Expr::Identifier(ident) => Some(ident), + _ => None, + }) + .map(ObjectNamePart::Identifier) + .chain(func.name.0) + .collect::>(); + func.name = ObjectName(compound_func_name); + + return Ok(Expr::Function(func)); + } + + // Flatten qualified outer join expressions. + // For example, the expression `T.foo(+)` should + // represent an outer join on the column name `T.foo` + // rather than a composite expression. + if access_chain.len() == 1 + && matches!( + access_chain.last(), + Some(AccessExpr::Dot(Expr::OuterJoin(_))) + ) + { + let Some(AccessExpr::Dot(Expr::OuterJoin(inner_expr))) = access_chain.pop() else { + return parser_err!("expected (+) expression", root.span().start); + }; + + if !Self::is_all_ident(&root, &[]) { + return parser_err!("column identifier before (+)", root.span().start); + }; + + let token_start = root.span().start; + let mut idents = Self::exprs_to_idents(root, vec![])?; + match *inner_expr { + Expr::CompoundIdentifier(suffix) => idents.extend(suffix), + Expr::Identifier(suffix) => idents.push(suffix), + _ => { + return parser_err!("column identifier before (+)", token_start); + } } - Ok(Expr::CompoundFieldAccess { - root: Box::new(root), - access_chain: chain.clone(), - }) + + return Ok(Expr::OuterJoin(Expr::CompoundIdentifier(idents).into())); + } + + Ok(Expr::CompoundFieldAccess { + root: Box::new(root), + access_chain, + }) + } + + fn keyword_to_modifier(k: Keyword) -> Option { + match k { + Keyword::LOCAL => Some(ContextModifier::Local), + Keyword::GLOBAL => Some(ContextModifier::Global), + Keyword::SESSION => Some(ContextModifier::Session), + _ => None, } } @@ -1620,20 +1981,23 @@ impl<'a> Parser<'a> { } } - /// Try to parse OuterJoin expression `(+)` - fn parse_outer_join_expr(&mut self, id_parts: &[Ident]) -> Option { - if dialect_of!(self is SnowflakeDialect | MsSqlDialect) - && self.consume_tokens(&[Token::LParen, Token::Plus, Token::RParen]) - { - Some(Expr::OuterJoin(Box::new( - match <[Ident; 1]>::try_from(id_parts.to_vec()) { - Ok([ident]) => Expr::Identifier(ident), - Err(parts) => Expr::CompoundIdentifier(parts), - }, - ))) - } else { - None + /// Returns true if the next tokens indicate the outer join operator `(+)`. + fn peek_outer_join_operator(&mut self) -> bool { + if !self.dialect.supports_outer_join_operator() { + return false; } + + let [maybe_lparen, maybe_plus, maybe_rparen] = self.peek_tokens_ref(); + Token::LParen == maybe_lparen.token + && Token::Plus == maybe_plus.token + && Token::RParen == maybe_rparen.token + } + + /// If the next tokens indicates the outer join operator `(+)`, consume + /// the tokens and return true. + fn maybe_parse_outer_join_operator(&mut self) -> bool { + self.dialect.supports_outer_join_operator() + && self.consume_tokens(&[Token::LParen, Token::Plus, Token::RParen]) } pub fn parse_utility_options(&mut self) -> Result, ParserError> { @@ -1683,42 +2047,51 @@ impl<'a> Parser<'a> { }) } - /// Parses method call expression - fn try_parse_method(&mut self, expr: Expr) -> Result { - if !self.dialect.supports_methods() { - return Ok(expr); - } - let method_chain = self.maybe_parse(|p| { - let mut method_chain = Vec::new(); - while p.consume_token(&Token::Period) { - let tok = p.next_token(); - let name = match tok.token { - Token::Word(word) => word.into_ident(tok.span), - _ => return p.expected("identifier", tok), - }; - let func = match p.parse_function(ObjectName(vec![name]))? { - Expr::Function(func) => func, - _ => return p.expected("function", p.peek_token()), - }; - method_chain.push(func); - } - if !method_chain.is_empty() { - Ok(method_chain) - } else { - p.expected("function", p.peek_token()) - } - })?; - if let Some(method_chain) = method_chain { - Ok(Expr::Method(Method { - expr: Box::new(expr), - method_chain, - })) - } else { - Ok(expr) + /// 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 function] call. + /// 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 /// /// ```sql @@ -1982,17 +2355,18 @@ 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()?)); self.expect_keyword_is(Keyword::WHEN)?; } let mut conditions = vec![]; - let mut results = vec![]; loop { - conditions.push(self.parse_expr()?); + let condition = self.parse_expr()?; self.expect_keyword_is(Keyword::THEN)?; - results.push(self.parse_expr()?); + let result = self.parse_expr()?; + conditions.push(CaseWhen { condition, result }); if !self.parse_keyword(Keyword::WHEN) { break; } @@ -2002,18 +2376,19 @@ 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, - results, else_result, }) } pub fn parse_optional_cast_format(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::FORMAT) { - let value = self.parse_value()?; + let value = self.parse_value()?.value; match self.parse_optional_time_zone()? { Some(tz) => Ok(Some(CastFormat::ValueAtTimeZone(value, tz))), None => Ok(Some(CastFormat::Value(value))), @@ -2025,7 +2400,7 @@ impl<'a> Parser<'a> { pub fn parse_optional_time_zone(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::AT, Keyword::TIME, Keyword::ZONE]) { - self.parse_value().map(Some) + self.parse_value().map(|v| Some(v.value)) } else { Ok(None) } @@ -2154,7 +2529,7 @@ impl<'a> Parser<'a> { CeilFloorKind::DateTimeField(self.parse_date_time_field()?) } else if self.consume_token(&Token::Comma) { // Parse `CEIL/FLOOR(expr, scale)` - match self.parse_value()? { + match self.parse_value()?.value { Value::Number(n, s) => CeilFloorKind::Scale(Value::Number(n, s)), _ => { return Err(ParserError::ParserError( @@ -2199,12 +2574,20 @@ impl<'a> Parser<'a> { Some(expr) => Ok(expr), // Snowflake supports `position` as an ordinary function call // without the special `IN` syntax. - None => self.parse_function(ObjectName(vec![ident])), + None => self.parse_function(ObjectName::from(vec![ident])), } } - pub fn parse_substring_expr(&mut self) -> Result { - // PARSE SUBSTRING (EXPR [FROM 1] [FOR 3]) + // { SUBSTRING | SUBSTR } ( [FROM 1] [FOR 3]) + pub fn parse_substring(&mut self) -> Result { + let shorthand = match self.expect_one_of_keywords(&[Keyword::SUBSTR, Keyword::SUBSTRING])? { + Keyword::SUBSTR => true, + Keyword::SUBSTRING => false, + _ => { + self.prev_token(); + return self.expected("SUBSTR or SUBSTRING", self.peek_token()); + } + }; self.expect_token(&Token::LParen)?; let expr = self.parse_expr()?; let mut from_expr = None; @@ -2224,6 +2607,7 @@ impl<'a> Parser<'a> { substring_from: from_expr.map(Box::new), substring_for: to_expr.map(Box::new), special, + shorthand, }) } @@ -2258,10 +2642,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()?); } } @@ -2277,7 +2658,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)?; @@ -2464,14 +2845,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) @@ -2483,14 +2864,14 @@ impl<'a> Parser<'a> { /// This method will raise an error if the column list is empty or with invalid identifiers, /// the match expression is not a literal string, or if the search modifier is not valid. pub fn parse_match_against(&mut self) -> Result { - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_qualified_column_list(Mandatory, false)?; self.expect_keyword_is(Keyword::AGAINST)?; self.expect_token(&Token::LParen)?; // MySQL is too permissive about the value, IMO we can't validate it perfectly on syntax level. - let match_value = self.parse_value()?; + let match_value = self.parse_value()?.value; let in_natural_language_mode_keywords = &[ Keyword::IN, @@ -2734,7 +3115,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. @@ -2747,16 +3127,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(( @@ -2776,6 +3150,7 @@ impl<'a> Parser<'a> { Ok(StructField { field_name: Some(field_name), field_type, + options: None, }) }); self.expect_token(&Token::RParen)?; @@ -2809,10 +3184,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, )) @@ -2844,7 +3221,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: /// @@ -2853,17 +3230,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_separated(Self::parse_duckdb_dictionary_field)?; + 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 /// @@ -2872,7 +3250,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)?; @@ -3006,11 +3385,13 @@ 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 { Token::Spaceship => Some(BinaryOperator::Spaceship), Token::DoubleEq => Some(BinaryOperator::Eq), + Token::Assignment => Some(BinaryOperator::Assignment), Token::Eq => Some(BinaryOperator::Eq), Token::Neq => Some(BinaryOperator::NotEq), Token::Gt => Some(BinaryOperator::Gt), @@ -3037,15 +3418,18 @@ impl<'a> Parser<'a> { Token::DuckIntDiv if dialect_is!(dialect is DuckDbDialect | GenericDialect) => { Some(BinaryOperator::DuckIntegerDivide) } - Token::ShiftLeft if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { + Token::ShiftLeft if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect | RedshiftSqlDialect) => { Some(BinaryOperator::PGBitwiseShiftLeft) } - Token::ShiftRight if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { + Token::ShiftRight if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect | RedshiftSqlDialect) => { Some(BinaryOperator::PGBitwiseShiftRight) } - Token::Sharp if dialect_is!(dialect is PostgreSqlDialect) => { + Token::Sharp if dialect_is!(dialect is PostgreSqlDialect | RedshiftSqlDialect) => { Some(BinaryOperator::PGBitwiseXor) } + Token::Overlap if dialect_is!(dialect is PostgreSqlDialect | RedshiftSqlDialect) => { + Some(BinaryOperator::PGOverlap) + } Token::Overlap if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { Some(BinaryOperator::PGOverlap) } @@ -3073,6 +3457,57 @@ impl<'a> Parser<'a> { Token::QuestionAnd => Some(BinaryOperator::QuestionAnd), Token::QuestionPipe => Some(BinaryOperator::QuestionPipe), Token::CustomBinaryOperator(s) => Some(BinaryOperator::Custom(s.clone())), + Token::DoubleSharp if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::DoubleHash) + } + + Token::AmpersandLeftAngleBracket if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::AndLt) + } + Token::AmpersandRightAngleBracket if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::AndGt) + } + Token::QuestionMarkDash if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::QuestionDash) + } + Token::AmpersandLeftAngleBracketVerticalBar + if self.dialect.supports_geometric_types() => + { + Some(BinaryOperator::AndLtPipe) + } + Token::VerticalBarAmpersandRightAngleBracket + if self.dialect.supports_geometric_types() => + { + Some(BinaryOperator::PipeAndGt) + } + Token::TwoWayArrow if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::LtDashGt) + } + Token::LeftAngleBracketCaret if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::LtCaret) + } + Token::RightAngleBracketCaret if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::GtCaret) + } + Token::QuestionMarkSharp if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::QuestionHash) + } + Token::QuestionMarkDoubleVerticalBar if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::QuestionDoublePipe) + } + Token::QuestionMarkDashVerticalBar if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::QuestionDashPipe) + } + Token::TildeEqual if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::TildeEq) + } + Token::ShiftLeftVerticalBar if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::LtLtPipe) + } + Token::VerticalBarShiftRight if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::PipeGtGt) + } + Token::AtSign if self.dialect.supports_geometric_types() => Some(BinaryOperator::At), Token::Word(w) => match w.keyword { Keyword::AND => Some(BinaryOperator::And), @@ -3127,10 +3562,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 ); @@ -3183,9 +3626,11 @@ impl<'a> Parser<'a> { { let expr2 = self.parse_expr()?; Ok(Expr::IsNotDistinctFrom(Box::new(expr), Box::new(expr2))) + } else if let Ok(is_normalized) = self.parse_unicode_is_normalized(expr) { + Ok(is_normalized) } else { self.expected( - "[NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS", + "[NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS", self.peek_token(), ) } @@ -3209,6 +3654,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, @@ -3218,6 +3668,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) { @@ -3255,6 +3707,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), @@ -3273,21 +3741,9 @@ impl<'a> Parser<'a> { op: UnaryOperator::PGPostfixFactorial, expr: Box::new(expr), }) - } else if Token::LBracket == *tok { - if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect) - { - let mut chain = vec![]; - // back to LBracket - self.prev_token(); - self.parse_multi_dim_subscript(&mut chain)?; - self.parse_compound_field_access(expr, chain) - } else if self.dialect.supports_partiql() { - self.prev_token(); - self.parse_json_access(expr) - } else { - parser_err!("Array subscripting is not supported", tok.span.start) - } - } else if dialect_of!(self is SnowflakeDialect | GenericDialect) && Token::Colon == *tok { + } else if Token::LBracket == *tok && self.dialect.supports_partiql() + || (dialect_of!(self is SnowflakeDialect | GenericDialect) && Token::Colon == *tok) + { self.prev_token(); self.parse_json_access(expr) } else { @@ -3300,9 +3756,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) } @@ -3475,15 +3931,13 @@ impl<'a> Parser<'a> { }); } self.expect_token(&Token::LParen)?; - let in_op = if self.parse_keyword(Keyword::SELECT) || self.parse_keyword(Keyword::WITH) { - self.prev_token(); - Expr::InSubquery { + let in_op = match self.maybe_parse(|p| p.parse_query())? { + Some(subquery) => Expr::InSubquery { expr: Box::new(expr), - subquery: self.parse_query()?, + subquery, negated, - } - } else { - Expr::InList { + }, + None => Expr::InList { expr: Box::new(expr), list: if self.dialect.supports_in_empty_list() { self.parse_comma_separated0(Parser::parse_expr, Token::RParen)? @@ -3491,7 +3945,7 @@ impl<'a> Parser<'a> { self.parse_comma_separated(Parser::parse_expr)? }, negated, - } + }, }; self.expect_token(&Token::RParen)?; Ok(in_op) @@ -3512,7 +3966,7 @@ impl<'a> Parser<'a> { }) } - /// Parse a postgresql casting style which is in the form of `expr::datatype`. + /// Parse a PostgreSQL casting style which is in the form of `expr::datatype`. pub fn parse_pg_cast(&mut self, expr: Expr) -> Result { Ok(Expr::Cast { kind: CastKind::DoubleColon, @@ -3597,6 +4051,26 @@ impl<'a> Parser<'a> { }) } + /// Returns references to the `N` next non-whitespace tokens + /// that have not yet been processed. + /// + /// See [`Self::peek_tokens`] for an example. + pub fn peek_tokens_ref(&self) -> [&TokenWithSpan; N] { + let mut index = self.index; + core::array::from_fn(|_| loop { + let token = self.tokens.get(index); + index += 1; + if let Some(TokenWithSpan { + token: Token::Whitespace(_), + span: _, + }) = token + { + continue; + } + break token.unwrap_or(&EOF_TOKEN); + }) + } + /// Return nth non-whitespace token that has not yet been processed pub fn peek_nth_token(&self, n: usize) -> TokenWithSpan { self.peek_nth_token_ref(n).clone() @@ -3780,6 +4254,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() { @@ -3787,10 +4273,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, @@ -3814,6 +4303,18 @@ impl<'a> Parser<'a> { true } + /// If the current token is one of the given `keywords`, returns the keyword + /// that matches, without consuming the token. Otherwise, returns [`None`]. + #[must_use] + pub fn peek_one_of_keywords(&self, keywords: &[Keyword]) -> Option { + for keyword in keywords { + if self.peek_keyword(*keyword) { + return Some(*keyword); + } + } + None + } + /// If the current token is one of the given `keywords`, consume the token /// and return the keyword that matches. Otherwise, no tokens are consumed /// and returns [`None`]. @@ -3850,7 +4351,7 @@ impl<'a> Parser<'a> { /// If the current token is the `expected` keyword, consume the token. /// Otherwise, return an error. /// - // todo deprecate infavor of expected_keyword_is + // todo deprecate in favor of expected_keyword_is pub fn expect_keyword(&mut self, expected: Keyword) -> Result { if self.parse_keyword(expected) { Ok(self.get_current_token().clone()) @@ -3943,11 +4444,11 @@ impl<'a> Parser<'a> { self.parse_comma_separated_with_trailing_commas( |p| p.parse_select_item(), trailing_commas, - None, + Self::is_reserved_for_column_alias, ) } - pub fn parse_actions_list(&mut self) -> Result, ParserError> { + pub fn parse_actions_list(&mut self) -> Result, ParserError> { let mut values = vec![]; loop { values.push(self.parse_grant_permission()?); @@ -3977,30 +4478,38 @@ impl<'a> Parser<'a> { self.parse_comma_separated_with_trailing_commas( Parser::parse_table_and_joins, trailing_commas, - Some(self.dialect.get_reserved_keywords_for_table_factor()), + |kw, parser| !self.dialect.is_table_factor(kw, parser), ) } /// Parse the comma of a comma-separated syntax element. + /// `R` is a predicate that should return true if the next + /// keyword is a reserved keyword. /// Allows for control over trailing commas + /// /// Returns true if there is a next element - fn is_parse_comma_separated_end_with_trailing_commas( + fn is_parse_comma_separated_end_with_trailing_commas( &mut self, trailing_commas: bool, - reserved_keywords: Option<&[Keyword]>, - ) -> bool { - let reserved_keywords = reserved_keywords.unwrap_or(keywords::RESERVED_FOR_COLUMN_ALIAS); + is_reserved_keyword: &R, + ) -> bool + where + R: Fn(&Keyword, &mut Parser) -> bool, + { if !self.consume_token(&Token::Comma) { true } else if trailing_commas { - let token = self.peek_token().token; - match token { - Token::Word(ref kw) if reserved_keywords.contains(&kw.keyword) => true, + let token = self.next_token().token; + let is_end = match token { + Token::Word(ref kw) if is_reserved_keyword(&kw.keyword, self) => true, Token::RParen | Token::SemiColon | Token::EOF | Token::RBracket | Token::RBrace => { true } _ => false, - } + }; + self.prev_token(); + + is_end } else { false } @@ -4009,7 +4518,10 @@ impl<'a> Parser<'a> { /// Parse the comma of a comma-separated syntax element. /// Returns true if there is a next element fn is_parse_comma_separated_end(&mut self) -> bool { - self.is_parse_comma_separated_end_with_trailing_commas(self.options.trailing_commas, None) + self.is_parse_comma_separated_end_with_trailing_commas( + self.options.trailing_commas, + &Self::is_reserved_for_column_alias, + ) } /// Parse a comma-separated list of 1+ items accepted by `F` @@ -4017,26 +4529,33 @@ impl<'a> Parser<'a> { where F: FnMut(&mut Parser<'a>) -> Result, { - self.parse_comma_separated_with_trailing_commas(f, self.options.trailing_commas, None) + self.parse_comma_separated_with_trailing_commas( + f, + self.options.trailing_commas, + Self::is_reserved_for_column_alias, + ) } - /// Parse a comma-separated list of 1+ items accepted by `F` - /// Allows for control over trailing commas - fn parse_comma_separated_with_trailing_commas( + /// Parse a comma-separated list of 1+ items accepted by `F`. + /// `R` is a predicate that should return true if the next + /// keyword is a reserved keyword. + /// Allows for control over trailing commas. + fn parse_comma_separated_with_trailing_commas( &mut self, mut f: F, trailing_commas: bool, - reserved_keywords: Option<&[Keyword]>, + is_reserved_keyword: R, ) -> Result, ParserError> where F: FnMut(&mut Parser<'a>) -> Result, + R: Fn(&Keyword, &mut Parser) -> bool, { let mut values = vec![]; loop { values.push(f(self)?); if self.is_parse_comma_separated_end_with_trailing_commas( trailing_commas, - reserved_keywords, + &is_reserved_keyword, ) { break; } @@ -4044,6 +4563,21 @@ impl<'a> Parser<'a> { Ok(values) } + /// Parse a period-separated list of 1+ items accepted by `F` + fn parse_period_separated(&mut self, mut f: F) -> Result, ParserError> + where + F: FnMut(&mut Parser<'a>) -> Result, + { + let mut values = vec![]; + loop { + values.push(f(self)?); + if !self.consume_token(&Token::Period) { + break; + } + } + Ok(values) + } + /// Parse a keyword-separated list of 1+ items accepted by `F` pub fn parse_keyword_separated( &mut self, @@ -4095,8 +4629,41 @@ impl<'a> Parser<'a> { self.parse_comma_separated(f) } + /// Parses 0 or more statements, each followed by a semicolon. + /// If the next token is any of `terminal_keywords` then no more + /// statements will be parsed. + pub(crate) fn parse_statement_list( + &mut self, + terminal_keywords: &[Keyword], + ) -> Result, ParserError> { + let mut values = vec![]; + loop { + 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)?; + } + Ok(values) + } + + /// Default implementation of a predicate that returns true if + /// the specified keyword is reserved for column alias. + /// See [Dialect::is_column_alias] + fn is_reserved_for_column_alias(kw: &Keyword, parser: &mut Parser) -> bool { + !parser.dialect.is_column_alias(kw, parser) + } + /// Run a parser method `f`, reverting back to the current position if unsuccessful. - /// Returns `None` if `f` returns an error + /// Returns `ParserError::RecursionLimitExceeded` if `f` returns a `RecursionLimitExceeded`. + /// Returns `Ok(None)` if `f` returns any other error. pub fn maybe_parse(&mut self, f: F) -> Result, ParserError> where F: FnMut(&mut Parser) -> Result, @@ -4174,23 +4741,30 @@ impl<'a> Parser<'a> { let create_view_params = self.parse_create_view_params()?; if self.parse_keyword(Keyword::TABLE) { self.parse_create_table(or_replace, temporary, global, transient) - } else if self.parse_keyword(Keyword::MATERIALIZED) || self.parse_keyword(Keyword::VIEW) { - self.prev_token(); - self.parse_create_view(or_replace, temporary, create_view_params) + } else if self.peek_keyword(Keyword::MATERIALIZED) + || self.peek_keyword(Keyword::VIEW) + || self.peek_keywords(&[Keyword::SECURE, Keyword::MATERIALIZED, Keyword::VIEW]) + || self.peek_keywords(&[Keyword::SECURE, Keyword::VIEW]) + { + self.parse_create_view(or_alter, or_replace, temporary, create_view_params) } else if self.parse_keyword(Keyword::POLICY) { self.parse_create_policy() } else if self.parse_keyword(Keyword::EXTERNAL) { self.parse_create_external_table(or_replace) } else if self.parse_keyword(Keyword::FUNCTION) { - self.parse_create_function(or_replace, temporary) + 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(temporary, 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(temporary, 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", @@ -4216,11 +4790,43 @@ impl<'a> Parser<'a> { self.parse_create_type() } else if self.parse_keyword(Keyword::PROCEDURE) { 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])? + .options; + let with_tags = self.parse_keyword(Keyword::WITH); + let tags = if self.parse_keyword(Keyword::TAG) { + self.parse_key_value_options(true, &[])?.options + } 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, @@ -4406,9 +5012,37 @@ impl<'a> Parser<'a> { let schema_name = self.parse_schema_name()?; + let default_collate_spec = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::COLLATE]) { + Some(self.parse_expr()?) + } else { + 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, }) } @@ -4443,11 +5077,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, }) } @@ -4475,6 +5131,7 @@ impl<'a> Parser<'a> { pub fn parse_create_function( &mut self, + or_alter: bool, or_replace: bool, temporary: bool, ) -> Result { @@ -4486,29 +5143,30 @@ impl<'a> Parser<'a> { self.parse_create_macro(or_replace, temporary) } else if dialect_of!(self is BigQueryDialect) { self.parse_bigquery_create_function(or_replace, temporary) + } else if dialect_of!(self is MsSqlDialect) { + self.parse_mssql_create_function(or_alter, or_replace, temporary) } else { self.prev_token(); self.expected("an object type after CREATE", self.peek_token()) } } - /// Parse `CREATE FUNCTION` for [Postgres] + /// Parse `CREATE FUNCTION` for [PostgreSQL] /// - /// [Postgres]: https://www.postgresql.org/docs/15/sql-createfunction.html + /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html fn parse_postgres_create_function( &mut self, or_replace: bool, temporary: bool, ) -> Result { let name = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; - let args = if self.consume_token(&Token::RParen) { - self.prev_token(); - None + let args = if Token::RParen != self.peek_token_ref().token { + self.parse_comma_separated(Parser::parse_function_arg)? } else { - Some(self.parse_comma_separated(Parser::parse_function_arg)?) + vec![] }; - self.expect_token(&Token::RParen)?; let return_type = if self.parse_keyword(Keyword::RETURNS) { @@ -4601,10 +5259,11 @@ impl<'a> Parser<'a> { } Ok(Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace, temporary, name, - args, + args: Some(args), return_type, behavior: body.behavior, called_on_null: body.called_on_null, @@ -4634,6 +5293,7 @@ impl<'a> Parser<'a> { let using = self.parse_optional_create_function_using()?; Ok(Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace, temporary, name, @@ -4661,22 +5321,7 @@ impl<'a> Parser<'a> { temporary: bool, ) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let name = self.parse_object_name(false)?; - - let parse_function_param = - |parser: &mut Parser| -> Result { - let name = parser.parse_identifier()?; - let data_type = parser.parse_data_type()?; - Ok(OperateFunctionArg { - mode: None, - name: Some(name), - data_type, - default_expr: None, - }) - }; - self.expect_token(&Token::LParen)?; - let args = self.parse_comma_separated0(parse_function_param, Token::RParen)?; - self.expect_token(&Token::RParen)?; + let (name, args) = self.parse_create_function_name_and_params()?; let return_type = if self.parse_keyword(Keyword::RETURNS) { Some(self.parse_data_type()?) @@ -4723,6 +5368,7 @@ impl<'a> Parser<'a> { }; Ok(Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace, temporary, if_not_exists, @@ -4741,54 +5387,185 @@ impl<'a> Parser<'a> { })) } - fn parse_function_arg(&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) + /// Parse `CREATE FUNCTION` for [MsSql] + /// + /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql + fn parse_mssql_create_function( + &mut self, + or_alter: bool, + or_replace: bool, + temporary: bool, + ) -> Result { + let (name, args) = self.parse_create_function_name_and_params()?; + + self.expect_keyword(Keyword::RETURNS)?; + + let return_table = self.maybe_parse(|p| { + let return_table_name = p.parse_identifier()?; + + p.expect_keyword_is(Keyword::TABLE)?; + p.prev_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 { - None + Some(self.parse_data_type()?) }; - // 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 - name = Some(n.0[0].clone()); - data_type = self.parse_data_type()?; - } + let _ = self.parse_keyword(Keyword::AS); - let default_expr = if self.parse_keyword(Keyword::DEFAULT) || self.consume_token(&Token::Eq) - { - Some(self.parse_expr()?) + 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 { - None + parser_err!("Unparsable function body", self.peek_token().span.start)? }; - Ok(OperateFunctionArg { - mode, - name, - data_type, - default_expr, - }) - } - /// Parse statements of the DropTrigger type such as: - /// - /// ```sql - /// DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] - /// ``` + Ok(Statement::CreateFunction(CreateFunction { + or_alter, + or_replace, + temporary, + if_not_exists: false, + name, + args: Some(args), + return_type, + function_body, + language: None, + determinism_specifier: None, + options: None, + remote_connection: None, + using: None, + behavior: None, + called_on_null: None, + parallel: None, + })) + } + + fn parse_create_function_name_and_params( + &mut self, + ) -> Result<(ObjectName, Vec), ParserError> { + let name = self.parse_object_name(false)?; + let parse_function_param = + |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, + }) + }; + self.expect_token(&Token::LParen)?; + let args = self.parse_comma_separated0(parse_function_param, Token::RParen)?; + self.expect_token(&Token::RParen)?; + Ok((name, args)) + } + + fn parse_function_arg(&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 + }; + + // parse: [ argname ] argtype + let mut name = None; + let mut data_type = self.parse_data_type()?; + + // 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()); + } + + 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) + { + Some(self.parse_expr()?) + } else { + None + }; + Ok(OperateFunctionArg { + mode, + name, + data_type, + default_expr, + }) + } + + /// Parse statements of the DropTrigger type such as: + /// + /// ```sql + /// DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] + /// ``` pub fn parse_drop_trigger(&mut self) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect) { + if !dialect_of!(self is PostgreSqlDialect | SQLiteDialect | GenericDialect | MySqlDialect | MsSqlDialect) + { self.prev_token(); return self.expected("an object type after DROP", self.peek_token()); } let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let trigger_name = self.parse_object_name(false)?; - self.expect_keyword_is(Keyword::ON)?; - let table_name = self.parse_object_name(false)?; + let table_name = if self.parse_keyword(Keyword::ON) { + Some(self.parse_object_name(false)?) + } else { + None + }; let option = self .parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) .map(|keyword| match keyword { @@ -4796,26 +5573,29 @@ impl<'a> Parser<'a> { Keyword::RESTRICT => ReferentialAction::Restrict, _ => unreachable!(), }); - Ok(Statement::DropTrigger { + Ok(Statement::DropTrigger(DropTrigger { if_exists, trigger_name, table_name, option, - }) + })) } pub fn parse_create_trigger( &mut self, + temporary: bool, + or_alter: bool, or_replace: bool, is_constraint: bool, ) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect) { + if !dialect_of!(self is PostgreSqlDialect | SQLiteDialect | GenericDialect | MySqlDialect | MsSqlDialect) + { self.prev_token(); return self.expected("an object type after CREATE", self.peek_token()); } let name = self.parse_object_name(false)?; - let period = self.parse_trigger_period()?; + let period = self.maybe_parse(|parser| parser.parse_trigger_period())?; let events = self.parse_keyword_separated(Keyword::OR, Parser::parse_trigger_event)?; self.expect_keyword_is(Keyword::ON)?; @@ -4836,48 +5616,70 @@ impl<'a> Parser<'a> { } } - self.expect_keyword_is(Keyword::FOR)?; - let include_each = self.parse_keyword(Keyword::EACH); - let trigger_object = - match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { - Keyword::ROW => TriggerObject::Row, - Keyword::STATEMENT => TriggerObject::Statement, - _ => unreachable!(), - }; + let trigger_object = if self.parse_keyword(Keyword::FOR) { + let include_each = self.parse_keyword(Keyword::EACH); + let trigger_object = + match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { + Keyword::ROW => TriggerObject::Row, + Keyword::STATEMENT => TriggerObject::Statement, + _ => unreachable!(), + }; + + Some(if include_each { + TriggerObjectKind::ForEach(trigger_object) + } else { + TriggerObjectKind::For(trigger_object) + }) + } else { + let _ = self.parse_keyword(Keyword::FOR); + + None + }; let condition = self .parse_keyword(Keyword::WHEN) .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 { + Ok(CreateTrigger { + or_alter, + temporary, or_replace, is_constraint, name, period, + period_before_table: true, events, table_name, referenced_table_name, referencing, trigger_object, - include_each, condition, exec_body, + statements_as: false, + statements, characteristics, - }) + } + .into()) } 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 @@ -5016,12 +5818,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) @@ -5047,6 +5854,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 { @@ -5062,18 +5877,25 @@ impl<'a> Parser<'a> { pub fn parse_create_view( &mut self, + or_alter: bool, or_replace: bool, temporary: bool, create_view_params: Option, ) -> Result { + let secure = self.parse_keyword(Keyword::SECURE); 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)?; @@ -5108,11 +5930,7 @@ impl<'a> Parser<'a> { && self.parse_keyword(Keyword::COMMENT) { self.expect_token(&Token::Eq)?; - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(str) => Some(str), - _ => self.expected("string literal", next_token)?, - } + Some(self.parse_comment_value()?) } else { None }; @@ -5129,11 +5947,13 @@ impl<'a> Parser<'a> { Keyword::BINDING, ]); - Ok(Statement::CreateView { + Ok(CreateView { + or_alter, name, columns, query, materialized, + secure, or_replace, options, cluster_by, @@ -5143,7 +5963,9 @@ impl<'a> Parser<'a> { temporary, to, params: create_view_params, - }) + name_before_not_exists, + } + .into()) } /// Parse optional parameters for the `CREATE VIEW` statement supported by [MySQL]. @@ -5406,7 +6228,7 @@ impl<'a> Parser<'a> { }? } - Ok(Statement::CreateRole { + Ok(CreateRole { names, if_not_exists, login, @@ -5425,7 +6247,8 @@ impl<'a> Parser<'a> { user, admin, authorization_owner, - }) + } + .into()) } pub fn parse_owner(&mut self) -> Result { @@ -5446,6 +6269,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 } ] @@ -5527,6 +6379,49 @@ impl<'a> Parser<'a> { }) } + /// ```sql + /// CREATE CONNECTOR [IF NOT EXISTS] connector_name + /// [TYPE datasource_type] + /// [URL datasource_url] + /// [COMMENT connector_comment] + /// [WITH DCPROPERTIES(property_name=property_value, ...)] + /// ``` + /// + /// [Hive Documentation](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-CreateDataConnectorCreateConnector) + pub fn parse_create_connector(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_identifier()?; + + let connector_type = if self.parse_keyword(Keyword::TYPE) { + Some(self.parse_literal_string()?) + } else { + None + }; + + let url = if self.parse_keyword(Keyword::URL) { + Some(self.parse_literal_string()?) + } else { + None + }; + + let comment = self.parse_optional_inline_comment()?; + + let with_dcproperties = + match self.parse_options_with_keywords(&[Keyword::WITH, Keyword::DCPROPERTIES])? { + properties if !properties.is_empty() => Some(properties), + _ => None, + }; + + Ok(Statement::CreateConnector(CreateConnector { + name, + if_not_exists, + connector_type, + url, + comment, + with_dcproperties, + })) + } + pub fn parse_drop(&mut self) -> Result { // MySQL dialect supports `TEMPORARY` let temporary = dialect_of!(self is MySqlDialect | GenericDialect | DuckDbDialect) @@ -5538,6 +6433,8 @@ impl<'a> Parser<'a> { ObjectType::Table } else if self.parse_keyword(Keyword::VIEW) { ObjectType::View + } else if self.parse_keywords(&[Keyword::MATERIALIZED, Keyword::VIEW]) { + ObjectType::MaterializedView } else if self.parse_keyword(Keyword::INDEX) { ObjectType::Index } else if self.parse_keyword(Keyword::ROLE) { @@ -5552,10 +6449,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) { @@ -5566,7 +6471,7 @@ impl<'a> Parser<'a> { return self.parse_drop_extension(); } else { return self.expected( - "DATABASE, EXTENSION, FUNCTION, INDEX, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, or 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(), ); }; @@ -5588,6 +6493,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, @@ -5596,6 +6506,7 @@ impl<'a> Parser<'a> { restrict, purge, temporary, + table, }) } @@ -5615,11 +6526,11 @@ impl<'a> Parser<'a> { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let func_desc = self.parse_comma_separated(Parser::parse_function_desc)?; let drop_behavior = self.parse_optional_drop_behavior(); - Ok(Statement::DropFunction { + Ok(Statement::DropFunction(DropFunction { if_exists, func_desc, drop_behavior, - }) + })) } /// ```sql @@ -5640,6 +6551,30 @@ impl<'a> Parser<'a> { drop_behavior, }) } + /// ```sql + /// DROP CONNECTOR [IF EXISTS] name + /// ``` + /// + /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-DropConnector) + fn parse_drop_connector(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier()?; + 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 [, ...] ] ) ] [, ...] @@ -5661,7 +6596,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)?; @@ -5941,7 +6876,7 @@ impl<'a> Parser<'a> { /// DECLARE // { // { @local_variable [AS] data_type [ = value ] } - // | { @cursor_variable_name CURSOR } + // | { @cursor_variable_name CURSOR [ FOR ] } // } [ ,...n ] /// ``` /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-local-variable-transact-sql?view=sql-server-ver16 @@ -5957,14 +6892,19 @@ impl<'a> Parser<'a> { /// ```text // { // { @local_variable [AS] data_type [ = value ] } - // | { @cursor_variable_name CURSOR } + // | { @cursor_variable_name CURSOR [ FOR ]} // } [ ,...n ] /// ``` /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-local-variable-transact-sql?view=sql-server-ver16 pub fn parse_mssql_declare_stmt(&mut self) -> Result { let name = { let ident = self.parse_identifier()?; - if !ident.value.starts_with('@') { + if !ident.value.starts_with('@') + && !matches!( + self.peek_token().token, + Token::Word(w) if w.keyword == Keyword::CURSOR + ) + { Err(ParserError::TokenizerError( "Invalid MsSql variable declaration.".to_string(), )) @@ -5988,7 +6928,14 @@ impl<'a> Parser<'a> { _ => (None, Some(self.parse_data_type()?)), }; - let assignment = self.parse_mssql_variable_declaration_expression()?; + let (for_query, assignment) = if self.peek_keyword(Keyword::FOR) { + self.next_token(); + let query = Some(self.parse_query()?); + (query, None) + } else { + let assignment = self.parse_mssql_variable_declaration_expression()?; + (None, assignment) + }; Ok(Declare { names: vec![name], @@ -5999,7 +6946,7 @@ impl<'a> Parser<'a> { sensitive: None, scroll: None, hold: None, - for_query: None, + for_query, }) } @@ -6060,11 +7007,11 @@ impl<'a> Parser<'a> { FetchDirection::Last } else if self.parse_keyword(Keyword::ABSOLUTE) { FetchDirection::Absolute { - limit: self.parse_number_value()?, + limit: self.parse_number_value()?.value, } } else if self.parse_keyword(Keyword::RELATIVE) { FetchDirection::Relative { - limit: self.parse_number_value()?, + limit: self.parse_number_value()?.value, } } else if self.parse_keyword(Keyword::FORWARD) { if self.parse_keyword(Keyword::ALL) { @@ -6072,7 +7019,7 @@ impl<'a> Parser<'a> { } else { FetchDirection::Forward { // TODO: Support optional - limit: Some(self.parse_number_value()?), + limit: Some(self.parse_number_value()?.value), } } } else if self.parse_keyword(Keyword::BACKWARD) { @@ -6081,18 +7028,26 @@ impl<'a> Parser<'a> { } else { FetchDirection::Backward { // TODO: Support optional - limit: Some(self.parse_number_value()?), + limit: Some(self.parse_number_value()?.value), } } } else if self.parse_keyword(Keyword::ALL) { FetchDirection::All } else { FetchDirection::Count { - limit: self.parse_number_value()?, + limit: self.parse_number_value()?.value, } }; - 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()?; @@ -6105,6 +7060,7 @@ impl<'a> Parser<'a> { Ok(Statement::Fetch { name, direction, + position, into, }) } @@ -6130,22 +7086,26 @@ impl<'a> Parser<'a> { pub fn parse_create_index(&mut self, unique: bool) -> Result { let concurrently = self.parse_keyword(Keyword::CONCURRENTLY); let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + + let mut using = None; + let index_name = if if_not_exists || !self.parse_keyword(Keyword::ON) { let index_name = self.parse_object_name(false)?; + // MySQL allows `USING index_type` either before or after `ON table_name` + using = self.parse_optional_using_then_index_type()?; self.expect_keyword_is(Keyword::ON)?; Some(index_name) } else { None }; + let table_name = self.parse_object_name(false)?; - let using = if self.parse_keyword(Keyword::USING) { - Some(self.parse_identifier()?) - } else { - None - }; - self.expect_token(&Token::LParen)?; - let columns = self.parse_comma_separated(Parser::parse_order_by_expr)?; - self.expect_token(&Token::RParen)?; + + // MySQL allows having two `USING` clauses. + // In that case, the second clause overwrites the first. + using = self.parse_optional_using_then_index_type()?.or(using); + + let columns = self.parse_parenthesized_index_column_list()?; let include = if self.parse_keyword(Keyword::INCLUDE) { self.expect_token(&Token::LParen)?; @@ -6181,6 +7141,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, @@ -6193,6 +7169,8 @@ impl<'a> Parser<'a> { nulls_distinct, with, predicate, + index_options, + alter_options, })) } @@ -6220,13 +7198,14 @@ impl<'a> Parser<'a> { (None, None, false) }; - Ok(Statement::CreateExtension { + Ok(CreateExtension { name, if_not_exists, schema, version, cascade, - }) + } + .into()) } /// Parse a PostgreSQL-specific [Statement::DropExtension] statement. @@ -6235,7 +7214,7 @@ impl<'a> Parser<'a> { let names = self.parse_comma_separated(|p| p.parse_identifier())?; let cascade_or_restrict = self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]); - Ok(Statement::DropExtension { + Ok(Statement::DropExtension(DropExtension { names, if_exists, cascade_or_restrict: cascade_or_restrict @@ -6245,7 +7224,7 @@ impl<'a> Parser<'a> { _ => self.expected("CASCADE or RESTRICT", self.peek_token()), }) .transpose()?, - }) + })) } //TODO: Implement parsing for Skewed @@ -6426,11 +7405,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() @@ -6440,17 +7415,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]); @@ -6458,39 +7432,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 @@ -6518,30 +7461,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 { @@ -6550,13 +7469,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()?) @@ -6573,8 +7485,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) @@ -6585,23 +7495,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) @@ -6617,13 +7562,32 @@ impl<'a> Parser<'a> { } } - /// Parse configuration like partitioning, clustering information during the table creation. + /// 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](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]) { @@ -6633,46 +7597,267 @@ 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); - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(str) => Some(if has_eq { - CommentDef::WithEq(str) - } else { - CommentDef::WithoutEq(str) - }), - _ => self.expected("comment", next_token)?, - } + let comment = self.parse_comment_value()?; + Some(if has_eq { + CommentDef::WithEq(comment) + } else { + CommentDef::WithoutEq(comment) + }) } else { None }; Ok(comment) } + pub fn parse_comment_value(&mut self) -> Result { + let next_token = self.next_token(); + let value = match next_token.token { + Token::SingleQuotedString(str) => str, + Token::DollarQuotedString(str) => str.value, + _ => self.expected("string literal", next_token)?, + }; + Ok(value) + } + pub fn parse_optional_procedure_parameters( &mut self, ) -> Result>, ParserError> { @@ -6718,7 +7903,11 @@ impl<'a> Parser<'a> { return self.expected("',' or ')' after column definition", self.peek_token()); }; - if rparen && (!comma || self.options.trailing_commas) { + if rparen + && (!comma + || self.dialect.supports_column_definition_trailing_commas() + || self.options.trailing_commas) + { let _ = self.consume_token(&Token::RParen); break; } @@ -6728,23 +7917,38 @@ 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 }) + let default = if self.consume_token(&Token::Eq) { + Some(self.parse_expr()?) + } else { + None + }; + + Ok(ProcedureParam { + name, + data_type, + mode, + default, + }) } pub fn parse_column_def(&mut self) -> Result { - let name = self.parse_identifier()?; + let col_name = self.parse_identifier()?; let data_type = if self.is_column_type_sqlite_unspecified() { DataType::Unspecified } else { self.parse_data_type()? }; - let mut collation = if self.parse_keyword(Keyword::COLLATE) { - Some(self.parse_object_name(false)?) - } else { - None - }; let mut options = vec![]; loop { if self.parse_keyword(Keyword::CONSTRAINT) { @@ -6759,18 +7963,13 @@ impl<'a> Parser<'a> { } } else if let Some(option) = self.parse_optional_column_option()? { options.push(ColumnOptionDef { name: None, option }); - } else if dialect_of!(self is MySqlDialect | SnowflakeDialect | GenericDialect) - && self.parse_keyword(Keyword::COLLATE) - { - collation = Some(self.parse_object_name(false)?); } else { break; }; } Ok(ColumnDef { - name, + name: col_name, data_type, - collation, options, }) } @@ -6803,30 +8002,43 @@ 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)?, ))) + } else if self.parse_keywords(&[Keyword::COLLATE]) { + Ok(Some(ColumnOption::Collation( + self.parse_object_name(false)?, + ))) } else if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) { Ok(Some(ColumnOption::NotNull)) } else if self.parse_keywords(&[Keyword::COMMENT]) { - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(value, ..) => Ok(Some(ColumnOption::Comment(value))), - _ => self.expected("string", next_token), - } + Ok(Some(ColumnOption::Comment(self.parse_comment_value()?))) } 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) { @@ -6835,29 +8047,52 @@ 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()?; - Ok(Some(ColumnOption::Unique { - is_primary: true, - characteristics, - })) + Ok(Some( + PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics, + } + .into(), + )) } else if self.parse_keyword(Keyword::UNIQUE) { let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(ColumnOption::Unique { - is_primary: false, - characteristics, - })) + Ok(Some( + UniqueConstraint { + name: None, + index_name: None, + index_type_display: KeyOrIndexDisplay::None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics, + nulls_distinct: NullsDistinctOption::None, + } + .into(), + )) } else if self.parse_keyword(Keyword::REFERENCES) { let foreign_table = self.parse_object_name(false)?; // PostgreSQL allows omitting the column list and // uses the primary key column of the foreign table by default let referred_columns = self.parse_parenthesized_column_list(Optional, false)?; + let mut match_kind = None; let mut on_delete = None; let mut on_update = None; loop { - if on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) { + if match_kind.is_none() && self.parse_keyword(Keyword::MATCH) { + match_kind = Some(self.parse_match_kind()?); + } else if on_delete.is_none() + && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) + { on_delete = Some(self.parse_referential_action()?); } else if on_update.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::UPDATE]) @@ -6869,18 +8104,33 @@ impl<'a> Parser<'a> { } let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(ColumnOption::ForeignKey { - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - })) + Ok(Some( + ForeignKeyConstraint { + name: None, // Column-level constraints don't have names + index_name: None, // Not applicable for column-level constraints + columns: vec![], // Not applicable for column-level constraints + foreign_table, + referred_columns, + on_delete, + on_update, + match_kind, + characteristics, + } + .into(), + )) } 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))) + Ok(Some( + CheckConstraint { + name: None, // Column-level check constraints don't have names + expr: Box::new(expr), + enforced: None, // Could be extended later to support MySQL ENFORCED/NOT ENFORCED + } + .into(), + )) } else if self.parse_keyword(Keyword::AUTO_INCREMENT) && dialect_of!(self is MySqlDialect | GenericDialect) { @@ -6912,7 +8162,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() @@ -6927,6 +8177,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) { @@ -6961,13 +8217,40 @@ impl<'a> Parser<'a> { Keyword::REPLACE, ])?, ))) + } else if self.parse_keyword(Keyword::INVISIBLE) { + Ok(Some(ColumnOption::Invisible)) } else { Ok(None) } } + /// 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()?; @@ -7010,7 +8293,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(( @@ -7083,7 +8366,7 @@ impl<'a> Parser<'a> { }; self.expect_keyword_is(Keyword::INTO)?; - let num_buckets = self.parse_number_value()?; + let num_buckets = self.parse_number_value()?.value; self.expect_keyword_is(Keyword::BUCKETS)?; Some(ClusteredBy { columns, @@ -7115,6 +8398,18 @@ impl<'a> Parser<'a> { } } + pub fn parse_match_kind(&mut self) -> Result { + if self.parse_keyword(Keyword::FULL) { + Ok(ConstraintReferenceMatchKind::Full) + } else if self.parse_keyword(Keyword::PARTIAL) { + Ok(ConstraintReferenceMatchKind::Partial) + } else if self.parse_keyword(Keyword::SIMPLE) { + Ok(ConstraintReferenceMatchKind::Simple) + } else { + self.expected("one of FULL, PARTIAL or SIMPLE", self.peek_token()) + } + } + pub fn parse_constraint_characteristics( &mut self, ) -> Result, ParserError> { @@ -7175,53 +8470,65 @@ 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 { - name, - index_name, - index_type_display, - index_type, - columns, - index_options, - characteristics, - nulls_distinct, - })) + Ok(Some( + UniqueConstraint { + name, + index_name, + index_type_display, + index_type, + columns, + index_options, + characteristics, + nulls_distinct, + } + .into(), + )) } Token::Word(w) if w.keyword == Keyword::PRIMARY => { // after `PRIMARY` always stay `KEY` 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 { - name, - index_name, - index_type, - columns, - index_options, - characteristics, - })) + Ok(Some( + PrimaryKeyConstraint { + name, + index_name, + index_type, + columns, + index_options, + characteristics, + } + .into(), + )) } 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)?; let referred_columns = self.parse_parenthesized_column_list(Optional, false)?; + let mut match_kind = None; let mut on_delete = None; let mut on_update = None; loop { - if on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) { + if match_kind.is_none() && self.parse_keyword(Keyword::MATCH) { + match_kind = Some(self.parse_match_kind()?); + } else if on_delete.is_none() + && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) + { on_delete = Some(self.parse_referential_action()?); } else if on_update.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::UPDATE]) @@ -7234,21 +8541,42 @@ impl<'a> Parser<'a> { let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(TableConstraint::ForeignKey { - name, - columns, - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - })) + Ok(Some( + ForeignKeyConstraint { + name, + index_name, + columns, + foreign_table, + referred_columns, + on_delete, + on_update, + match_kind, + characteristics, + } + .into(), + )) } Token::Word(w) if w.keyword == Keyword::CHECK => { 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( + CheckConstraint { + name, + expr, + enforced, + } + .into(), + )) } Token::Word(w) if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) @@ -7259,18 +8587,23 @@ 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, - })) + Ok(Some( + IndexConstraint { + display_as_key, + name, + index_type, + columns, + index_options, + } + .into(), + )) } Token::Word(w) if (w.keyword == Keyword::FULLTEXT || w.keyword == Keyword::SPATIAL) @@ -7290,16 +8623,19 @@ 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, - index_type_display, - opt_index_name, - columns, - })) + Ok(Some( + FullTextOrSpatialConstraint { + fulltext, + index_type_display, + opt_index_name, + columns, + } + .into(), + )) } _ => { if name.is_some() { @@ -7341,7 +8677,7 @@ impl<'a> Parser<'a> { pub fn parse_options(&mut self, keyword: Keyword) -> Result, ParserError> { if self.parse_keyword(keyword) { self.expect_token(&Token::LParen)?; - let options = self.parse_comma_separated(Parser::parse_sql_option)?; + let options = self.parse_comma_separated0(Parser::parse_sql_option, Token::RParen)?; self.expect_token(&Token::RParen)?; Ok(options) } else { @@ -7364,16 +8700,30 @@ impl<'a> Parser<'a> { } pub fn parse_index_type(&mut self) -> Result { - if self.parse_keyword(Keyword::BTREE) { - Ok(IndexType::BTree) + Ok(if self.parse_keyword(Keyword::BTREE) { + IndexType::BTree } else if self.parse_keyword(Keyword::HASH) { - Ok(IndexType::Hash) - } else { - self.expected("index type {BTREE | HASH}", self.peek_token()) - } + IndexType::Hash + } else if self.parse_keyword(Keyword::GIN) { + IndexType::GIN + } else if self.parse_keyword(Keyword::GIST) { + IndexType::GiST + } else if self.parse_keyword(Keyword::SPGIST) { + IndexType::SPGiST + } else if self.parse_keyword(Keyword::BRIN) { + IndexType::BRIN + } else if self.parse_keyword(Keyword::BLOOM) { + IndexType::Bloom + } else { + IndexType::Custom(self.parse_identifier()?) + }) } - /// Parse [USING {BTREE | HASH}] + /// Optionally parse the `USING` keyword, followed by an [IndexType] + /// Example: + /// ```sql + //// USING BTREE (name, age DESC) + /// ``` pub fn parse_optional_using_then_index_type( &mut self, ) -> Result, ParserError> { @@ -7386,7 +8736,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()) } @@ -7545,7 +8895,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) { @@ -7597,7 +8951,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()?; @@ -7706,10 +9067,19 @@ impl<'a> Parser<'a> { name, drop_behavior, } - } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) - && dialect_of!(self is MySqlDialect | GenericDialect) - { - AlterTableOperation::DropPrimaryKey + } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) { + 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()?; + 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) { @@ -7719,12 +9089,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, } @@ -7797,16 +9172,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) @@ -7898,23 +9267,104 @@ impl<'a> Parser<'a> { AlterTableOperation::SuspendRecluster } else if self.parse_keywords(&[Keyword::RESUME, Keyword::RECLUSTER]) { AlterTableOperation::ResumeRecluster + } else if self.parse_keyword(Keyword::LOCK) { + let equals = self.consume_token(&Token::Eq); + let lock = match self.parse_one_of_keywords(&[ + Keyword::DEFAULT, + Keyword::EXCLUSIVE, + Keyword::NONE, + Keyword::SHARED, + ]) { + Some(Keyword::DEFAULT) => AlterTableLock::Default, + Some(Keyword::EXCLUSIVE) => AlterTableLock::Exclusive, + Some(Keyword::NONE) => AlterTableLock::None, + Some(Keyword::SHARED) => AlterTableLock::Shared, + _ => self.expected( + "DEFAULT, EXCLUSIVE, NONE or SHARED after LOCK [=]", + self.peek_token(), + )?, + }; + AlterTableOperation::Lock { equals, lock } + } else if self.parse_keyword(Keyword::ALGORITHM) { + let equals = self.consume_token(&Token::Eq); + let algorithm = match self.parse_one_of_keywords(&[ + Keyword::DEFAULT, + Keyword::INSTANT, + Keyword::INPLACE, + Keyword::COPY, + ]) { + Some(Keyword::DEFAULT) => AlterTableAlgorithm::Default, + Some(Keyword::INSTANT) => AlterTableAlgorithm::Instant, + Some(Keyword::INPLACE) => AlterTableAlgorithm::Inplace, + Some(Keyword::COPY) => AlterTableAlgorithm::Copy, + _ => self.expected( + "DEFAULT, INSTANT, INPLACE, or COPY after ALGORITHM [=]", + self.peek_token(), + )?, + }; + AlterTableOperation::Algorithm { equals, algorithm } + } else if self.parse_keyword(Keyword::AUTO_INCREMENT) { + 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 { @@ -7928,41 +9378,28 @@ impl<'a> Parser<'a> { pub fn parse_alter(&mut self) -> Result { let object_type = self.expect_one_of_keywords(&[ Keyword::VIEW, + Keyword::TYPE, Keyword::TABLE, Keyword::INDEX, Keyword::ROLE, Keyword::POLICY, + Keyword::CONNECTOR, + Keyword::ICEBERG, + Keyword::SCHEMA, + Keyword::USER, ])?; match object_type { + Keyword::SCHEMA => { + self.prev_token(); + self.prev_token(); + self.parse_alter_schema() + } Keyword::VIEW => self.parse_alter_view(), - 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::TYPE => self.parse_alter_type(), + 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)?; @@ -7977,16 +9414,62 @@ impl<'a> Parser<'a> { return self.expected("RENAME after ALTER INDEX", self.peek_token()); }; - Ok(Statement::AlterIndex { - name: index_name, - operation, - }) - } - Keyword::ROLE => self.parse_alter_role(), - Keyword::POLICY => self.parse_alter_policy(), - // unreachable because expect_one_of_keywords used above - _ => unreachable!(), + Ok(Statement::AlterIndex { + name: index_name, + operation, + }) + } + Keyword::ROLE => self.parse_alter_role(), + Keyword::POLICY => self.parse_alter_policy(), + Keyword::CONNECTOR => self.parse_alter_connector(), + Keyword::USER => self.parse_alter_user(), + // unreachable because expect_one_of_keywords used above + _ => unreachable!(), + } + } + + /// 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(AlterTable { + name: table_name, + if_exists, + only, + operations, + location, + on_cluster, + table_type: if iceberg { + Some(AlterTableType::Iceberg) + } else { + None + }, + end_token: AttachedToken(end_token), } + .into()) } pub fn parse_alter_view(&mut self) -> Result { @@ -8006,6 +9489,95 @@ impl<'a> Parser<'a> { }) } + /// Parse a [Statement::AlterType] + pub fn parse_alter_type(&mut self) -> Result { + let name = self.parse_object_name(false)?; + + if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + let new_name = self.parse_identifier()?; + Ok(Statement::AlterType(AlterType { + name, + operation: AlterTypeOperation::Rename(AlterTypeRename { new_name }), + })) + } else if self.parse_keywords(&[Keyword::ADD, Keyword::VALUE]) { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let new_enum_value = self.parse_identifier()?; + let position = if self.parse_keyword(Keyword::BEFORE) { + Some(AlterTypeAddValuePosition::Before(self.parse_identifier()?)) + } else if self.parse_keyword(Keyword::AFTER) { + Some(AlterTypeAddValuePosition::After(self.parse_identifier()?)) + } else { + None + }; + + Ok(Statement::AlterType(AlterType { + name, + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists, + value: new_enum_value, + position, + }), + })) + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::VALUE]) { + let existing_enum_value = self.parse_identifier()?; + self.expect_keyword(Keyword::TO)?; + let new_enum_value = self.parse_identifier()?; + + Ok(Statement::AlterType(AlterType { + name, + operation: AlterTypeOperation::RenameValue(AlterTypeRenameValue { + from: existing_enum_value, + to: new_enum_value, + }), + })) + } else { + 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 if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + let new_name = self.parse_object_name(false)?; + AlterSchemaOperation::Rename { name: new_name } + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + let owner = self.parse_owner()?; + AlterSchemaOperation::OwnerTo { owner } + } 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 { @@ -8099,6 +9671,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 @@ -8154,20 +9734,77 @@ 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::ADDQUOTES, + Keyword::ALLOWOVERWRITE, Keyword::BINARY, + Keyword::BLANKSASNULL, + Keyword::BZIP2, + Keyword::CLEANPATH, + Keyword::COMPUPDATE, + Keyword::CSV, + Keyword::DATEFORMAT, Keyword::DELIMITER, + Keyword::EMPTYASNULL, + Keyword::ENCRYPTED, + Keyword::ESCAPE, + Keyword::EXTENSION, + Keyword::FIXEDWIDTH, + Keyword::GZIP, + Keyword::HEADER, + Keyword::IAM_ROLE, + Keyword::IGNOREHEADER, + Keyword::JSON, + Keyword::MANIFEST, + Keyword::MAXFILESIZE, Keyword::NULL, - Keyword::CSV, + Keyword::PARALLEL, + Keyword::PARQUET, + Keyword::PARTITION, + Keyword::REGION, + Keyword::REMOVEQUOTES, + Keyword::ROWGROUPSIZE, + Keyword::STATUPDATE, + Keyword::TIMEFORMAT, + Keyword::TRUNCATECOLUMNS, + Keyword::ZSTD, ]) { - Some(Keyword::BINARY) => CopyLegacyOption::Binary, - Some(Keyword::DELIMITER) => { + Some(Keyword::ACCEPTANYDATE) => CopyLegacyOption::AcceptAnyDate, + Some(Keyword::ACCEPTINVCHARS) => { let _ = self.parse_keyword(Keyword::AS); // [ AS ] - CopyLegacyOption::Delimiter(self.parse_literal_char()?) + let ch = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) { + Some(self.parse_literal_string()?) + } else { + None + }; + CopyLegacyOption::AcceptInvChars(ch) } - Some(Keyword::NULL) => { - let _ = self.parse_keyword(Keyword::AS); // [ AS ] - CopyLegacyOption::Null(self.parse_literal_string()?) + Some(Keyword::ADDQUOTES) => CopyLegacyOption::AddQuotes, + Some(Keyword::ALLOWOVERWRITE) => CopyLegacyOption::AllowOverwrite, + Some(Keyword::BINARY) => CopyLegacyOption::Binary, + Some(Keyword::BLANKSASNULL) => CopyLegacyOption::BlankAsNull, + Some(Keyword::BZIP2) => CopyLegacyOption::Bzip2, + Some(Keyword::CLEANPATH) => CopyLegacyOption::CleanPath, + Some(Keyword::COMPUPDATE) => { + let preset = self.parse_keyword(Keyword::PRESET); + let enabled = match self.parse_one_of_keywords(&[ + Keyword::TRUE, + Keyword::FALSE, + Keyword::ON, + Keyword::OFF, + ]) { + Some(Keyword::TRUE) | Some(Keyword::ON) => Some(true), + Some(Keyword::FALSE) | Some(Keyword::OFF) => Some(false), + _ => None, + }; + CopyLegacyOption::CompUpdate { preset, enabled } } Some(Keyword::CSV) => CopyLegacyOption::Csv({ let mut opts = vec![]; @@ -8178,11 +9815,143 @@ 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::ENCRYPTED) => { + let auto = self.parse_keyword(Keyword::AUTO); + CopyLegacyOption::Encrypted { auto } + } + Some(Keyword::ESCAPE) => CopyLegacyOption::Escape, + Some(Keyword::EXTENSION) => { + let ext = self.parse_literal_string()?; + CopyLegacyOption::Extension(ext) + } + Some(Keyword::FIXEDWIDTH) => { + let spec = self.parse_literal_string()?; + CopyLegacyOption::FixedWidth(spec) + } + Some(Keyword::GZIP) => CopyLegacyOption::Gzip, + Some(Keyword::HEADER) => CopyLegacyOption::Header, + 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::JSON) => CopyLegacyOption::Json, + Some(Keyword::MANIFEST) => { + let verbose = self.parse_keyword(Keyword::VERBOSE); + CopyLegacyOption::Manifest { verbose } + } + Some(Keyword::MAXFILESIZE) => { + let _ = self.parse_keyword(Keyword::AS); + let size = self.parse_number_value()?.value; + let unit = match self.parse_one_of_keywords(&[Keyword::MB, Keyword::GB]) { + Some(Keyword::MB) => Some(FileSizeUnit::MB), + Some(Keyword::GB) => Some(FileSizeUnit::GB), + _ => None, + }; + CopyLegacyOption::MaxFileSize(FileSize { size, unit }) + } + Some(Keyword::NULL) => { + let _ = self.parse_keyword(Keyword::AS); + CopyLegacyOption::Null(self.parse_literal_string()?) + } + Some(Keyword::PARALLEL) => { + let enabled = match self.parse_one_of_keywords(&[ + Keyword::TRUE, + Keyword::FALSE, + Keyword::ON, + Keyword::OFF, + ]) { + Some(Keyword::TRUE) | Some(Keyword::ON) => Some(true), + Some(Keyword::FALSE) | Some(Keyword::OFF) => Some(false), + _ => None, + }; + CopyLegacyOption::Parallel(enabled) + } + Some(Keyword::PARQUET) => CopyLegacyOption::Parquet, + Some(Keyword::PARTITION) => { + self.expect_keyword(Keyword::BY)?; + let columns = self.parse_parenthesized_column_list(IsOptional::Mandatory, false)?; + let include = self.parse_keyword(Keyword::INCLUDE); + CopyLegacyOption::PartitionBy(UnloadPartitionBy { columns, include }) + } + Some(Keyword::REGION) => { + let _ = self.parse_keyword(Keyword::AS); + let region = self.parse_literal_string()?; + CopyLegacyOption::Region(region) + } + Some(Keyword::REMOVEQUOTES) => CopyLegacyOption::RemoveQuotes, + Some(Keyword::ROWGROUPSIZE) => { + let _ = self.parse_keyword(Keyword::AS); + let file_size = self.parse_file_size()?; + CopyLegacyOption::RowGroupSize(file_size) + } + Some(Keyword::STATUPDATE) => { + let enabled = match self.parse_one_of_keywords(&[ + Keyword::TRUE, + Keyword::FALSE, + Keyword::ON, + Keyword::OFF, + ]) { + Some(Keyword::TRUE) | Some(Keyword::ON) => Some(true), + Some(Keyword::FALSE) | Some(Keyword::OFF) => Some(false), + _ => None, + }; + CopyLegacyOption::StatUpdate(enabled) + } + 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, + Some(Keyword::ZSTD) => CopyLegacyOption::Zstd, _ => self.expected("option", self.peek_token())?, }; Ok(ret) } + fn parse_file_size(&mut self) -> Result { + let size = self.parse_number_value()?.value; + let unit = self.maybe_parse_file_size_unit(); + Ok(FileSize { size, unit }) + } + + fn maybe_parse_file_size_unit(&mut self) -> Option { + match self.parse_one_of_keywords(&[Keyword::MB, Keyword::GB]) { + Some(Keyword::MB) => Some(FileSizeUnit::MB), + Some(Keyword::GB) => Some(FileSizeUnit::GB), + _ => None, + } + } + + 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, @@ -8264,21 +10033,22 @@ impl<'a> Parser<'a> { } /// Parse a literal value (numbers, strings, date/time, booleans) - pub fn parse_value(&mut self) -> Result { + pub fn parse_value(&mut self) -> Result { let next_token = self.next_token(); let span = next_token.span; + let ok_value = |value: Value| Ok(value.with_span(span)); match next_token.token { Token::Word(w) => match w.keyword { Keyword::TRUE if self.dialect.supports_boolean_literals() => { - Ok(Value::Boolean(true)) + ok_value(Value::Boolean(true)) } Keyword::FALSE if self.dialect.supports_boolean_literals() => { - Ok(Value::Boolean(false)) + ok_value(Value::Boolean(false)) } - Keyword::NULL => Ok(Value::Null), + Keyword::NULL => ok_value(Value::Null), Keyword::NoKeyword if w.quote_style.is_some() => match w.quote_style { - Some('"') => Ok(Value::DoubleQuotedString(w.value)), - Some('\'') => Ok(Value::SingleQuotedString(w.value)), + Some('"') => ok_value(Value::DoubleQuotedString(w.value)), + Some('\'') => ok_value(Value::SingleQuotedString(w.value)), _ => self.expected( "A value?", TokenWithSpan { @@ -8298,56 +10068,71 @@ impl<'a> Parser<'a> { // The call to n.parse() returns a bigdecimal when the // bigdecimal feature is enabled, and is otherwise a no-op // (i.e., it returns the input string). - Token::Number(n, l) => Ok(Value::Number(Self::parse(n, span.start)?, l)), - Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), - Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())), + Token::Number(n, l) => ok_value(Value::Number(Self::parse(n, span.start)?, l)), + Token::SingleQuotedString(ref s) => ok_value(Value::SingleQuotedString( + self.maybe_concat_string_literal(s.to_string()), + )), + Token::DoubleQuotedString(ref s) => ok_value(Value::DoubleQuotedString( + self.maybe_concat_string_literal(s.to_string()), + )), Token::TripleSingleQuotedString(ref s) => { - Ok(Value::TripleSingleQuotedString(s.to_string())) + ok_value(Value::TripleSingleQuotedString(s.to_string())) } Token::TripleDoubleQuotedString(ref s) => { - Ok(Value::TripleDoubleQuotedString(s.to_string())) + ok_value(Value::TripleDoubleQuotedString(s.to_string())) } - Token::DollarQuotedString(ref s) => Ok(Value::DollarQuotedString(s.clone())), + Token::DollarQuotedString(ref s) => ok_value(Value::DollarQuotedString(s.clone())), Token::SingleQuotedByteStringLiteral(ref s) => { - Ok(Value::SingleQuotedByteStringLiteral(s.clone())) + ok_value(Value::SingleQuotedByteStringLiteral(s.clone())) } Token::DoubleQuotedByteStringLiteral(ref s) => { - Ok(Value::DoubleQuotedByteStringLiteral(s.clone())) + ok_value(Value::DoubleQuotedByteStringLiteral(s.clone())) } Token::TripleSingleQuotedByteStringLiteral(ref s) => { - Ok(Value::TripleSingleQuotedByteStringLiteral(s.clone())) + ok_value(Value::TripleSingleQuotedByteStringLiteral(s.clone())) } Token::TripleDoubleQuotedByteStringLiteral(ref s) => { - Ok(Value::TripleDoubleQuotedByteStringLiteral(s.clone())) + ok_value(Value::TripleDoubleQuotedByteStringLiteral(s.clone())) } Token::SingleQuotedRawStringLiteral(ref s) => { - Ok(Value::SingleQuotedRawStringLiteral(s.clone())) + ok_value(Value::SingleQuotedRawStringLiteral(s.clone())) } Token::DoubleQuotedRawStringLiteral(ref s) => { - Ok(Value::DoubleQuotedRawStringLiteral(s.clone())) + ok_value(Value::DoubleQuotedRawStringLiteral(s.clone())) } Token::TripleSingleQuotedRawStringLiteral(ref s) => { - Ok(Value::TripleSingleQuotedRawStringLiteral(s.clone())) + ok_value(Value::TripleSingleQuotedRawStringLiteral(s.clone())) } Token::TripleDoubleQuotedRawStringLiteral(ref s) => { - Ok(Value::TripleDoubleQuotedRawStringLiteral(s.clone())) + ok_value(Value::TripleDoubleQuotedRawStringLiteral(s.clone())) + } + Token::NationalStringLiteral(ref s) => { + ok_value(Value::NationalStringLiteral(s.to_string())) + } + Token::EscapedStringLiteral(ref s) => { + ok_value(Value::EscapedStringLiteral(s.to_string())) } - Token::NationalStringLiteral(ref s) => Ok(Value::NationalStringLiteral(s.to_string())), - Token::EscapedStringLiteral(ref s) => Ok(Value::EscapedStringLiteral(s.to_string())), - Token::UnicodeStringLiteral(ref s) => Ok(Value::UnicodeStringLiteral(s.to_string())), - Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), - Token::Placeholder(ref s) => Ok(Value::Placeholder(s.to_string())), + Token::UnicodeStringLiteral(ref s) => { + ok_value(Value::UnicodeStringLiteral(s.to_string())) + } + 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::Placeholder(placeholder)) + Ok(Value::Placeholder(tok.to_string() + &ident.value) + .with_span(Span::new(span.start, ident.span.end))) } unexpected => self.expected( "a value", @@ -8359,11 +10144,24 @@ impl<'a> Parser<'a> { } } + fn maybe_concat_string_literal(&mut self, mut str: String) -> String { + if self.dialect.supports_string_literal_concatenation() { + while let Token::SingleQuotedString(ref s) | Token::DoubleQuotedString(ref s) = + self.peek_token_ref().token + { + str.push_str(s.clone().as_str()); + self.advance_token(); + } + } + str + } + /// Parse an unsigned numeric literal - pub fn parse_number_value(&mut self) -> Result { - match self.parse_value()? { - v @ Value::Number(_, _) => Ok(v), - v @ Value::Placeholder(_) => Ok(v), + pub fn parse_number_value(&mut self) -> Result { + let value_wrapper = self.parse_value()?; + match &value_wrapper.value { + Value::Number(_, _) => Ok(value_wrapper), + Value::Placeholder(_) => Ok(value_wrapper), _ => { self.prev_token(); self.expected("literal number", self.peek_token()) @@ -8391,13 +10189,19 @@ impl<'a> Parser<'a> { } } - fn parse_introduced_string_value(&mut self) -> Result { + fn parse_introduced_string_expr(&mut self) -> Result { let next_token = self.next_token(); let span = next_token.span; match next_token.token { - Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), - Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())), - Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), + Token::SingleQuotedString(ref s) => Ok(Expr::Value( + Value::SingleQuotedString(s.to_string()).with_span(span), + )), + Token::DoubleQuotedString(ref s) => Ok(Expr::Value( + Value::DoubleQuotedString(s.to_string()).with_span(span), + )), + Token::HexStringLiteral(ref s) => Ok(Expr::Value( + Value::HexStringLiteral(s.to_string()).with_span(span), + )), unexpected => self.expected( "a string value", TokenWithSpan { @@ -8421,15 +10225,16 @@ impl<'a> Parser<'a> { /// e.g. `CREATE FUNCTION ... AS $$ body $$`. fn parse_create_function_body_string(&mut self) -> Result { let peek_token = self.peek_token(); + let span = peek_token.span; match peek_token.token { Token::DollarQuotedString(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { self.next_token(); - Ok(Expr::Value(Value::DollarQuotedString(s))) + Ok(Expr::Value(Value::DollarQuotedString(s).with_span(span))) } - _ => Ok(Expr::Value(Value::SingleQuotedString( - self.parse_literal_string()?, - ))), + _ => Ok(Expr::Value( + Value::SingleQuotedString(self.parse_literal_string()?).with_span(span), + )), } } @@ -8452,6 +10257,42 @@ 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); + let normalized_form = self.maybe_parse(|parser| { + match parser.parse_one_of_keywords(&[ + Keyword::NFC, + Keyword::NFD, + Keyword::NFKC, + Keyword::NFKD, + ]) { + Some(Keyword::NFC) => Ok(NormalizationForm::NFC), + Some(Keyword::NFD) => Ok(NormalizationForm::NFD), + Some(Keyword::NFKC) => Ok(NormalizationForm::NFKC), + Some(Keyword::NFKD) => Ok(NormalizationForm::NFKD), + _ => parser.expected("unicode normalization form", parser.peek_token()), + } + })?; + if self.parse_keyword(Keyword::NORMALIZED) { + return Ok(Expr::IsNormalized { + expr: Box::new(expr), + form: normalized_form, + negated: neg, + }); + } + self.expected("unicode normalization form", self.peek_token()) + } + pub fn parse_enum_values(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; let values = self.parse_comma_separated(|parser| { @@ -8495,33 +10336,58 @@ impl<'a> Parser<'a> { Token::Word(w) => match w.keyword { Keyword::BOOLEAN => Ok(DataType::Boolean), Keyword::BOOL => Ok(DataType::Bool), - Keyword::FLOAT => Ok(DataType::Float(self.parse_optional_precision()?)), - Keyword::REAL => Ok(DataType::Real), + Keyword::FLOAT => { + let precision = self.parse_exact_number_optional_precision_scale()?; + + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::FloatUnsigned(precision)) + } else { + Ok(DataType::Float(precision)) + } + } + Keyword::REAL => { + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::RealUnsigned) + } else { + Ok(DataType::Real) + } + } Keyword::FLOAT4 => Ok(DataType::Float4), Keyword::FLOAT32 => Ok(DataType::Float32), Keyword::FLOAT64 => Ok(DataType::Float64), Keyword::FLOAT8 => Ok(DataType::Float8), Keyword::DOUBLE => { if self.parse_keyword(Keyword::PRECISION) { - Ok(DataType::DoublePrecision) + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::DoublePrecisionUnsigned) + } else { + Ok(DataType::DoublePrecision) + } } else { - Ok(DataType::Double( - self.parse_exact_number_optional_precision_scale()?, - )) + let precision = self.parse_exact_number_optional_precision_scale()?; + + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::DoubleUnsigned(precision)) + } else { + Ok(DataType::Double(precision)) + } } } Keyword::TINYINT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedTinyInt(optional_precision?)) + Ok(DataType::TinyIntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::TinyInt(optional_precision?)) } } Keyword::INT2 => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInt2(optional_precision?)) + Ok(DataType::Int2Unsigned(optional_precision?)) } else { Ok(DataType::Int2(optional_precision?)) } @@ -8529,31 +10395,40 @@ impl<'a> Parser<'a> { Keyword::SMALLINT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedSmallInt(optional_precision?)) + Ok(DataType::SmallIntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::SmallInt(optional_precision?)) } } Keyword::MEDIUMINT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedMediumInt(optional_precision?)) + Ok(DataType::MediumIntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::MediumInt(optional_precision?)) } } Keyword::INT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInt(optional_precision?)) + Ok(DataType::IntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::Int(optional_precision?)) } } Keyword::INT4 => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInt4(optional_precision?)) + Ok(DataType::Int4Unsigned(optional_precision?)) } else { Ok(DataType::Int4(optional_precision?)) } @@ -8561,7 +10436,7 @@ impl<'a> Parser<'a> { Keyword::INT8 => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInt8(optional_precision?)) + Ok(DataType::Int8Unsigned(optional_precision?)) } else { Ok(DataType::Int8(optional_precision?)) } @@ -8574,19 +10449,30 @@ impl<'a> Parser<'a> { Keyword::INTEGER => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInteger(optional_precision?)) + Ok(DataType::IntegerUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::Integer(optional_precision?)) } } Keyword::BIGINT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedBigInt(optional_precision?)) + Ok(DataType::BigIntUnsigned(optional_precision?)) } else { + if dialect.supports_data_type_signed_suffix() { + let _ = self.parse_keyword(Keyword::SIGNED); + } Ok(DataType::BigInt(optional_precision?)) } } + Keyword::HUGEINT => Ok(DataType::HugeInt), + Keyword::UBIGINT => Ok(DataType::UBigInt), + Keyword::UHUGEINT => Ok(DataType::UHugeInt), + Keyword::USMALLINT => Ok(DataType::USmallInt), + Keyword::UTINYINT => Ok(DataType::UTinyInt), Keyword::UINT8 => Ok(DataType::UInt8), Keyword::UINT16 => Ok(DataType::UInt16), Keyword::UINT32 => Ok(DataType::UInt32), @@ -8623,7 +10509,7 @@ impl<'a> Parser<'a> { } Keyword::CLOB => Ok(DataType::Clob(self.parse_optional_precision()?)), Keyword::BINARY => Ok(DataType::Binary(self.parse_optional_precision()?)), - Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_optional_precision()?)), + Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_optional_binary_length()?)), Keyword::BLOB => Ok(DataType::Blob(self.parse_optional_precision()?)), Keyword::TINYBLOB => Ok(DataType::TinyBlob), Keyword::MEDIUMBLOB => Ok(DataType::MediumBlob), @@ -8636,6 +10522,7 @@ impl<'a> Parser<'a> { Ok(DataType::Bit(self.parse_optional_precision()?)) } } + Keyword::VARBIT => Ok(DataType::VarBit(self.parse_optional_precision()?)), Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), Keyword::DATE32 => Ok(DataType::Date32), @@ -8662,6 +10549,9 @@ impl<'a> Parser<'a> { self.parse_optional_precision()?, TimezoneInfo::Tz, )), + Keyword::TIMESTAMP_NTZ => { + Ok(DataType::TimestampNtz(self.parse_optional_precision()?)) + } Keyword::TIME => { let precision = self.parse_optional_precision()?; let tz = if self.parse_keyword(Keyword::WITH) { @@ -8679,10 +10569,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), @@ -8701,12 +10599,24 @@ impl<'a> Parser<'a> { Keyword::NUMERIC => Ok(DataType::Numeric( self.parse_exact_number_optional_precision_scale()?, )), - Keyword::DECIMAL => Ok(DataType::Decimal( - self.parse_exact_number_optional_precision_scale()?, - )), - Keyword::DEC => Ok(DataType::Dec( - self.parse_exact_number_optional_precision_scale()?, - )), + Keyword::DECIMAL => { + let precision = self.parse_exact_number_optional_precision_scale()?; + + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::DecimalUnsigned(precision)) + } else { + Ok(DataType::Decimal(precision)) + } + } + Keyword::DEC => { + let precision = self.parse_exact_number_optional_precision_scale()?; + + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::DecUnsigned(precision)) + } else { + Ok(DataType::Dec(precision)) + } + } Keyword::BIGNUMERIC => Ok(DataType::BigNumeric( self.parse_exact_number_optional_precision_scale()?, )), @@ -8783,6 +10693,36 @@ impl<'a> Parser<'a> { let _ = self.parse_keyword(Keyword::TYPE); Ok(DataType::AnyType) } + Keyword::TABLE => { + // 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) { + Ok(DataType::SignedInteger) + } else { + Ok(DataType::Signed) + } + } + Keyword::UNSIGNED => { + if self.parse_keyword(Keyword::INTEGER) { + Ok(DataType::UnsignedInteger) + } else { + 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)?; @@ -8796,20 +10736,28 @@ impl<'a> Parser<'a> { _ => self.expected_at("a data type name", next_token_index), }?; - // Parse array data types. Note: this is postgresql-specific and different from - // Keyword::ARRAY syntax from above - while self.consume_token(&Token::LBracket) { - let size = if dialect_of!(self is GenericDialect | DuckDbDialect | PostgreSqlDialect) { - self.maybe_parse(|p| p.parse_literal_uint())? - } else { - None - }; - self.expect_token(&Token::RBracket)?; - data = DataType::Array(ArrayElemTypeDef::SquareBracket(Box::new(data), size)) + if self.dialect.supports_array_typedef_with_brackets() { + while self.consume_token(&Token::LBracket) { + // Parse optional array data type size + let size = self.maybe_parse(|p| p.parse_literal_uint())?; + self.expect_token(&Token::RBracket)?; + data = DataType::Array(ArrayElemTypeDef::SquareBracket(Box::new(data), size)) + } } Ok((data, trailing_bracket)) } + fn parse_returns_table_column(&mut self) -> Result { + self.parse_column_def() + } + + fn parse_returns_table_columns(&mut self) -> Result, ParserError> { + self.expect_token(&Token::LParen)?; + let columns = self.parse_comma_separated(Parser::parse_returns_table_column)?; + self.expect_token(&Token::RParen)?; + Ok(columns) + } + pub fn parse_string_values(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; let mut values = Vec::new(); @@ -8829,46 +10777,184 @@ impl<'a> Parser<'a> { Ok(values) } - /// Strictly parse `identifier AS identifier` - pub fn parse_identifier_with_alias(&mut self) -> Result { - let ident = self.parse_identifier()?; - self.expect_keyword_is(Keyword::AS)?; - let alias = self.parse_identifier()?; - Ok(IdentWithAlias { ident, alias }) + /// Strictly parse `identifier AS identifier` + pub fn parse_identifier_with_alias(&mut self) -> Result { + let ident = self.parse_identifier()?; + self.expect_keyword_is(Keyword::AS)?; + let alias = self.parse_identifier()?; + 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 { + parser.dialect.is_select_item_alias(explicit, kw, parser) + } + self.parse_optional_alias_inner(None, validator) + } + + /// Optionally parses an alias for a table like in `... FROM generate_series(1, 10) AS t (col)`. + /// In this case, the alias is allowed to optionally name the columns in the table, in + /// addition to the table itself. + pub fn maybe_parse_table_alias(&mut self) -> Result, ParserError> { + fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + parser.dialect.is_table_factor_alias(explicit, kw, parser) + } + match self.parse_optional_alias_inner(None, validator)? { + Some(name) => { + let columns = self.parse_table_alias_column_defs()?; + Ok(Some(TableAlias { name, columns })) + } + None => Ok(None), + } + } + + fn parse_table_index_hints(&mut self) -> Result, ParserError> { + let mut hints = vec![]; + while let Some(hint_type) = + self.parse_one_of_keywords(&[Keyword::USE, Keyword::IGNORE, Keyword::FORCE]) + { + let hint_type = match hint_type { + Keyword::USE => TableIndexHintType::Use, + Keyword::IGNORE => TableIndexHintType::Ignore, + Keyword::FORCE => TableIndexHintType::Force, + _ => { + return self.expected( + "expected to match USE/IGNORE/FORCE keyword", + self.peek_token(), + ) + } + }; + let index_type = match self.parse_one_of_keywords(&[Keyword::INDEX, Keyword::KEY]) { + Some(Keyword::INDEX) => TableIndexType::Index, + Some(Keyword::KEY) => TableIndexType::Key, + _ => { + return self.expected("expected to match INDEX/KEY keyword", self.peek_token()) + } + }; + let for_clause = if self.parse_keyword(Keyword::FOR) { + let clause = if self.parse_keyword(Keyword::JOIN) { + TableIndexHintForClause::Join + } else if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + TableIndexHintForClause::OrderBy + } else if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { + TableIndexHintForClause::GroupBy + } else { + return self.expected( + "expected to match FOR/ORDER BY/GROUP BY table hint in for clause", + self.peek_token(), + ); + }; + Some(clause) + } else { + None + }; + + self.expect_token(&Token::LParen)?; + let index_names = if self.peek_token().token != Token::RParen { + self.parse_comma_separated(Parser::parse_identifier)? + } else { + vec![] + }; + self.expect_token(&Token::RParen)?; + hints.push(TableIndexHints { + hint_type, + index_type, + for_clause, + index_names, + }); + } + Ok(hints) } - /// Parse `AS identifier` (or simply `identifier` if it's not a reserved keyword) - /// Some examples with aliases: `SELECT 1 foo`, `SELECT COUNT(*) AS cnt`, - /// `SELECT ... FROM t1 foo, t2 bar`, `SELECT ... FROM (...) AS bar` + /// Wrapper for parse_optional_alias_inner, left for backwards-compatibility + /// but new flows should use the context-specific methods such as `maybe_parse_select_item_alias` + /// and `maybe_parse_table_alias`. pub fn parse_optional_alias( &mut self, reserved_kwds: &[Keyword], ) -> Result, ParserError> { + fn validator(_explicit: bool, _kw: &Keyword, _parser: &mut Parser) -> bool { + false + } + self.parse_optional_alias_inner(Some(reserved_kwds), validator) + } + + /// Parses an optional alias after a SQL element such as a select list item + /// or a table name. + /// + /// This method accepts an optional list of reserved keywords or a function + /// to call to validate if a keyword should be parsed as an alias, to allow + /// callers to customize the parsing logic based on their context. + fn parse_optional_alias_inner( + &mut self, + reserved_kwds: Option<&[Keyword]>, + validator: F, + ) -> Result, ParserError> + where + F: Fn(bool, &Keyword, &mut Parser) -> bool, + { let after_as = self.parse_keyword(Keyword::AS); + let next_token = self.next_token(); match next_token.token { - // Accept any identifier after `AS` (though many dialects have restrictions on - // keywords that may appear here). If there's no `AS`: don't parse keywords, - // which may start a construct allowed in this position, to be parsed as aliases. - // (For example, in `FROM t1 JOIN` the `JOIN` will always be parsed as a keyword, - // not an alias.) - Token::Word(w) if after_as || !reserved_kwds.contains(&w.keyword) => { + // By default, if a word is located after the `AS` keyword we consider it an alias + // as long as it's not reserved. + Token::Word(w) + if after_as || reserved_kwds.is_some_and(|x| !x.contains(&w.keyword)) => + { Ok(Some(w.into_ident(next_token.span))) } - // MSSQL supports single-quoted strings as aliases for columns - // We accept them as table aliases too, although MSSQL does not. - // - // Note, that this conflicts with an obscure rule from the SQL - // standard, which we don't implement: - // https://crate.io/docs/sql-99/en/latest/chapters/07.html#character-string-literal-s - // "[Obscure Rule] SQL allows you to break a long up into two or more smaller s, split by a that includes a newline - // character. When it sees such a , your DBMS will - // ignore the and treat the multiple strings as - // a single ." + // This pattern allows for customizing the acceptance of words as aliases based on the caller's + // context, such as to what SQL element this word is a potential alias of (select item alias, table name + // alias, etc.) or dialect-specific logic that goes beyond a simple list of reserved keywords. + Token::Word(w) if validator(after_as, &w.keyword, self) => { + Ok(Some(w.into_ident(next_token.span))) + } + // For backwards-compatibility, we accept quoted strings as aliases regardless of the context. Token::SingleQuotedString(s) => Ok(Some(Ident::with_quote('\'', s))), - // Support for MySql dialect double-quoted string, `AS "HOUR"` for example Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))), _ => { if after_as { @@ -8880,23 +10966,6 @@ impl<'a> Parser<'a> { } } - /// Parse `AS identifier` when the AS is describing a table-valued object, - /// like in `... FROM generate_series(1, 10) AS t (col)`. In this case - /// the alias is allowed to optionally name the columns in the table, in - /// addition to the table itself. - pub fn parse_optional_table_alias( - &mut self, - reserved_kwds: &[Keyword], - ) -> Result, ParserError> { - match self.parse_optional_alias(reserved_kwds)? { - Some(name) => { - let columns = self.parse_table_alias_column_defs()?; - Ok(Some(TableAlias { name, columns })) - } - None => Ok(None), - } - } - pub fn parse_optional_group_by(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { let expressions = if self.parse_keyword(Keyword::ALL) { @@ -8906,7 +10975,7 @@ impl<'a> Parser<'a> { }; let mut modifiers = vec![]; - if dialect_of!(self is ClickHouseDialect | GenericDialect) { + if self.dialect.supports_group_by_with_modifier() { loop { if !self.parse_keyword(Keyword::WITH) { break; @@ -8929,6 +10998,20 @@ impl<'a> Parser<'a> { }); } } + if self.parse_keywords(&[Keyword::GROUPING, Keyword::SETS]) { + self.expect_token(&Token::LParen)?; + 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, + ))); + }; let group_by = match expressions { None => GroupByExpr::All(modifiers), Some(exprs) => GroupByExpr::Expressions(exprs, modifiers), @@ -8941,23 +11024,86 @@ impl<'a> Parser<'a> { pub fn parse_optional_order_by(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { - let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; - let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) { - self.parse_interpolations()? + let order_by = + if self.dialect.supports_order_by_all() && self.parse_keyword(Keyword::ALL) { + let order_by_options = self.parse_order_by_options()?; + OrderBy { + kind: OrderByKind::All(order_by_options), + interpolate: None, + } + } else { + let exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; + let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) { + self.parse_interpolations()? + } else { + None + }; + OrderBy { + kind: OrderByKind::Expressions(exprs), + interpolate, + } + }; + Ok(Some(order_by)) + } else { + Ok(None) + } + } + + fn parse_optional_limit_clause(&mut self) -> Result, ParserError> { + let mut offset = if self.parse_keyword(Keyword::OFFSET) { + Some(self.parse_offset()?) + } else { + None + }; + + let (limit, limit_by) = if self.parse_keyword(Keyword::LIMIT) { + let expr = self.parse_limit()?; + + if self.dialect.supports_limit_comma() + && offset.is_none() + && expr.is_some() // ALL not supported with comma + && self.consume_token(&Token::Comma) + { + let offset = expr.ok_or_else(|| { + ParserError::ParserError( + "Missing offset for LIMIT , ".to_string(), + ) + })?; + return Ok(Some(LimitClause::OffsetCommaLimit { + offset, + limit: self.parse_expr()?, + })); + } + + let limit_by = if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.parse_keyword(Keyword::BY) + { + Some(self.parse_comma_separated(Parser::parse_expr)?) } else { None }; - Ok(Some(OrderBy { - exprs: order_by_exprs, - interpolate, + (Some(expr), limit_by) + } else { + (None, None) + }; + + if offset.is_none() && limit.is_some() && self.parse_keyword(Keyword::OFFSET) { + offset = Some(self.parse_offset()?); + } + + if offset.is_some() || (limit.is_some() && limit != Some(None)) || limit_by.is_some() { + Ok(Some(LimitClause::LimitOffset { + limit: limit.unwrap_or_default(), + offset, + limit_by: limit_by.unwrap_or_default(), })) } else { Ok(None) } } - /// Parse a table object for insetion + /// Parse a table object for insertion /// e.g. `some_database.some_table` or `FUNCTION some_table_func(...)` pub fn parse_table_object(&mut self) -> Result { if self.dialect.supports_insert_table_function() && self.parse_keyword(Keyword::FUNCTION) { @@ -8969,83 +11115,111 @@ 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(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(|ident| ident.value.contains('.')) + && parts.iter().any(|part| { + part.as_ident() + .is_some_and(|ident| ident.value.contains('.')) + }) { - idents = idents + parts = parts .into_iter() - .flat_map(|ident| { - ident + .flat_map(|part| match part.as_ident() { + Some(ident) => ident .value .split('.') - .map(|value| Ident { - value: value.into(), - quote_style: ident.quote_style, - span: ident.span, + .map(|value| { + ObjectNamePart::Identifier(Ident { + value: value.into(), + quote_style: ident.quote_style, + span: ident.span, + }) }) - .collect::>() + .collect::>(), + None => vec![part], }) .collect() } - Ok(ObjectName(idents)) + Ok(ObjectName(parts)) } /// Parse identifiers @@ -9250,7 +11424,11 @@ impl<'a> Parser<'a> { self.next_token(); Ok(vec![]) } else { - let cols = self.parse_comma_separated(Parser::parse_view_column)?; + let cols = self.parse_comma_separated_with_trailing_commas( + Parser::parse_view_column, + self.dialect.supports_column_definition_trailing_commas(), + Self::is_reserved_for_column_alias, + )?; self.expect_token(&Token::RParen)?; Ok(cols) } @@ -9262,17 +11440,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 { @@ -9285,18 +11453,84 @@ impl<'a> Parser<'a> { }) } - /// Parse a parenthesized comma-separated list of unqualified, possibly quoted identifiers + 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( &mut self, optional: IsOptional, allow_empty: bool, ) -> Result, ParserError> { + 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( + &mut self, + optional: IsOptional, + allow_empty: bool, + ) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { + p.parse_object_name(true) + }) + } + + /// Parses a parenthesized comma-separated list of columns using + /// the provided function to parse each element. + fn parse_parenthesized_column_list_inner( + &mut self, + optional: IsOptional, + allow_empty: bool, + mut f: F, + ) -> Result, ParserError> + where + F: FnMut(&mut Parser) -> Result, + { if self.consume_token(&Token::LParen) { if allow_empty && self.peek_token().token == Token::RParen { self.next_token(); Ok(vec![]) } else { - let cols = self.parse_comma_separated(|p| p.parse_identifier())?; + let cols = self.parse_comma_separated(|p| f(p))?; self.expect_token(&Token::RParen)?; Ok(cols) } @@ -9307,7 +11541,7 @@ impl<'a> Parser<'a> { } } - /// Parse a parenthesized comma-separated list of table alias column definitions. + /// Parses a parenthesized comma-separated list of table alias column definitions. fn parse_table_alias_column_defs(&mut self) -> Result, ParserError> { if self.consume_token(&Token::LParen) { let cols = self.parse_comma_separated(|p| { @@ -9339,6 +11573,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 @@ -9371,6 +11684,16 @@ impl<'a> Parser<'a> { } } + pub fn parse_optional_binary_length(&mut self) -> Result, ParserError> { + if self.consume_token(&Token::LParen) { + let binary_length = self.parse_binary_length()?; + self.expect_token(&Token::RParen)?; + Ok(Some(binary_length)) + } else { + Ok(None) + } + } + pub fn parse_character_length(&mut self) -> Result { if self.parse_keyword(Keyword::MAX) { return Ok(CharacterLength::Max); @@ -9386,6 +11709,14 @@ impl<'a> Parser<'a> { Ok(CharacterLength::IntegerLength { length, unit }) } + pub fn parse_binary_length(&mut self) -> Result { + if self.parse_keyword(Keyword::MAX) { + return Ok(BinaryLength::Max); + } + let length = self.parse_literal_uint()?; + Ok(BinaryLength::IntegerLength { length }) + } + pub fn parse_optional_precision_scale( &mut self, ) -> Result<(Option, Option), ParserError> { @@ -9409,7 +11740,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 }; @@ -9425,6 +11756,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(); @@ -9462,6 +11814,20 @@ impl<'a> Parser<'a> { Ok(parent_type(inside_type.into())) } + /// Parse a DELETE statement, returning a `Box`ed SetExpr + /// + /// This is used to reduce the size of the stack frames in debug builds + fn parse_delete_setexpr_boxed(&mut self) -> Result, ParserError> { + 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. @@ -9571,7 +11937,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()?); } } @@ -9635,71 +12001,64 @@ impl<'a> Parser<'a> { Ok(Query { with, body: self.parse_insert_setexpr_boxed()?, - limit: None, - limit_by: vec![], order_by: None, - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], } .into()) } else if self.parse_keyword(Keyword::UPDATE) { Ok(Query { with, body: self.parse_update_setexpr_boxed()?, - limit: None, - limit_by: vec![], order_by: None, - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], } .into()) - } else { - let body = self.parse_query_body(self.dialect.prec_unknown())?; - - let order_by = self.parse_optional_order_by()?; - - let mut limit = None; - let mut offset = None; - - for _x in 0..2 { - if limit.is_none() && self.parse_keyword(Keyword::LIMIT) { - limit = self.parse_limit()? - } - - if offset.is_none() && self.parse_keyword(Keyword::OFFSET) { - offset = Some(self.parse_offset()?) - } - - if self.dialect.supports_limit_comma() - && limit.is_some() - && offset.is_none() - && self.consume_token(&Token::Comma) - { - // MySQL style LIMIT x,y => LIMIT y OFFSET x. - // Check for more details. - offset = Some(Offset { - value: limit.unwrap(), - rows: OffsetRows::None, - }); - limit = Some(self.parse_expr()?); - } + } else if self.parse_keyword(Keyword::DELETE) { + Ok(Query { + with, + body: self.parse_delete_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 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 { + let body = self.parse_query_body(self.dialect.prec_unknown())?; - let limit_by = if dialect_of!(self is ClickHouseDialect | GenericDialect) - && self.parse_keyword(Keyword::BY) - { - self.parse_comma_separated(Parser::parse_expr)? - } else { - vec![] - }; + let order_by = self.parse_optional_order_by()?; + + let limit_clause = self.parse_optional_limit_clause()?; let settings = self.parse_settings()?; @@ -9732,23 +12091,247 @@ 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, order_by, - limit, - limit_by, - offset, + limit_clause, fetch, locks, 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) @@ -9756,7 +12339,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()?; + let value = p.parse_expr()?; Ok(Setting { key, value }) })?; Some(key_values) @@ -9939,7 +12522,9 @@ impl<'a> Parser<'a> { pub fn parse_query_body(&mut self, precedence: u8) -> Result, ParserError> { // We parse the expression using a Pratt parser, as in `parse_expr()`. // Start by parsing a restricted SELECT or a `(subquery)`: - let expr = if self.peek_keyword(Keyword::SELECT) { + let expr = if self.peek_keyword(Keyword::SELECT) + || (self.peek_keyword(Keyword::FROM) && self.dialect.supports_from_first_select()) + { SetExpr::Select(self.parse_select().map(Box::new)?) } else if self.consume_token(&Token::LParen) { // CTEs are not allowed here, but the parser currently accepts them @@ -9948,7 +12533,10 @@ impl<'a> Parser<'a> { SetExpr::Query(subquery) } else if self.parse_keyword(Keyword::VALUES) { let is_mysql = dialect_of!(self is MySqlDialect); - SetExpr::Values(self.parse_values(is_mysql)?) + SetExpr::Values(self.parse_values(is_mysql, false)?) + } else if self.parse_keyword(Keyword::VALUE) { + let is_mysql = dialect_of!(self is MySqlDialect); + SetExpr::Values(self.parse_values(is_mysql, true)?) } else if self.parse_keyword(Keyword::TABLE) { SetExpr::Table(Box::new(self.parse_as_table()?)) } else { @@ -10038,19 +12626,42 @@ impl<'a> Parser<'a> { /// Parse a restricted `SELECT` statement (no CTEs / `UNION` / `ORDER BY`) pub fn parse_select(&mut self) -> Result { + let mut from_first = None; + + if self.dialect.supports_from_first_select() && self.peek_keyword(Keyword::FROM) { + let from_token = self.expect_keyword(Keyword::FROM)?; + let from = self.parse_table_with_joins()?; + if !self.peek_keyword(Keyword::SELECT) { + return Ok(Select { + select_token: AttachedToken(from_token), + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![], + exclude: None, + into: None, + from, + 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::FromFirstNoSelect, + }); + } + from_first = Some(from); + } + 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; @@ -10070,19 +12681,14 @@ 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) { - let temporary = self - .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) - .is_some(); - let unlogged = self.parse_keyword(Keyword::UNLOGGED); - let table = self.parse_keyword(Keyword::TABLE); - let name = self.parse_object_name(false)?; - Some(SelectInto { - temporary, - unlogged, - table, - name, - }) + Some(self.parse_select_into()?) } else { None }; @@ -10092,10 +12698,12 @@ impl<'a> Parser<'a> { // otherwise they may be parsed as an alias as part of the `projection` // or `from`. - let from = if self.parse_keyword(Keyword::FROM) { - self.parse_table_with_joins()? + let (from, from_first) = if let Some(from) = from_first.take() { + (from, true) + } else if self.parse_keyword(Keyword::FROM) { + (self.parse_table_with_joins()?, false) } else { - vec![] + (vec![], false) }; let mut lateral_views = vec![]; @@ -10160,7 +12768,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![] }; @@ -10212,6 +12820,7 @@ impl<'a> Parser<'a> { top, top_before_distinct, projection, + exclude, into, from, lateral_views, @@ -10227,9 +12836,40 @@ impl<'a> Parser<'a> { qualify, value_table_mode, connect_by, + flavor: if from_first { + SelectFlavor::FromFirst + } else { + SelectFlavor::Standard + }, }) } + 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. @@ -10313,147 +12953,244 @@ impl<'a> Parser<'a> { } /// Parse a `SET ROLE` statement. Expects SET to be consumed already. - fn parse_set_role(&mut self, modifier: Option) -> Result { + fn parse_set_role( + &mut self, + modifier: Option, + ) -> Result { self.expect_keyword_is(Keyword::ROLE)?; - let context_modifier = match modifier { - Some(Keyword::LOCAL) => ContextModifier::Local, - Some(Keyword::SESSION) => ContextModifier::Session, - _ => ContextModifier::None, - }; let role_name = if self.parse_keyword(Keyword::NONE) { None } else { Some(self.parse_identifier()?) }; - Ok(Statement::SetRole { - context_modifier, + Ok(Statement::Set(Set::SetRole { + context_modifier: modifier, role_name, - }) + })) } - pub fn parse_set(&mut self) -> Result { - let modifier = - self.parse_one_of_keywords(&[Keyword::SESSION, Keyword::LOCAL, Keyword::HIVEVAR]); - if let Some(Keyword::HIVEVAR) = modifier { - self.expect_token(&Token::Colon)?; - } else if let Some(set_role_stmt) = - self.maybe_parse(|parser| parser.parse_set_role(modifier))? - { - return Ok(set_role_stmt); + fn parse_set_values( + &mut self, + parenthesized_assignment: bool, + ) -> Result, ParserError> { + let mut values = vec![]; + + if parenthesized_assignment { + self.expect_token(&Token::LParen)?; + } + + loop { + let value = if let Some(expr) = self.try_parse_expr_sub_query()? { + expr + } else if let Ok(expr) = self.parse_expr() { + expr + } else { + self.expected("variable value", self.peek_token())? + }; + + values.push(value); + if self.consume_token(&Token::Comma) { + continue; + } + + if parenthesized_assignment { + self.expect_token(&Token::RParen)?; + } + return Ok(values); } + } + + fn parse_context_modifier(&mut self) -> Option { + let modifier = + self.parse_one_of_keywords(&[Keyword::SESSION, Keyword::LOCAL, Keyword::GLOBAL])?; + + Self::keyword_to_modifier(modifier) + } + + /// Parse a single SET statement assignment `var = expr`. + fn parse_set_assignment(&mut self) -> Result { + let scope = self.parse_context_modifier(); - let variables = if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) { - OneOrManyWithParens::One(ObjectName(vec!["TIMEZONE".into()])) - } else if self.dialect.supports_parenthesized_set_variables() + let name = if self.dialect.supports_parenthesized_set_variables() && self.consume_token(&Token::LParen) { - let variables = OneOrManyWithParens::Many( - self.parse_comma_separated(|parser: &mut Parser<'a>| parser.parse_identifier())? - .into_iter() - .map(|ident| ObjectName(vec![ident])) - .collect(), - ); - self.expect_token(&Token::RParen)?; - variables + // Parenthesized assignments are handled in the `parse_set` function after + // trying to parse list of assignments using this function. + // If a dialect supports both, and we find a LParen, we early exit from this function. + self.expected("Unparenthesized assignment", self.peek_token())? } else { - OneOrManyWithParens::One(self.parse_object_name(false)?) + self.parse_object_name(false)? + }; + + if !(self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO)) { + return self.expected("assignment operator", self.peek_token()); + } + + let value = self.parse_expr()?; + + Ok(SetAssignment { scope, name, value }) + } + + fn parse_set(&mut self) -> Result { + let hivevar = self.parse_keyword(Keyword::HIVEVAR); + + // Modifier is either HIVEVAR: or a ContextModifier (LOCAL, SESSION, etc), not both + let scope = if !hivevar { + self.parse_context_modifier() + } else { + None }; - if matches!(&variables, OneOrManyWithParens::One(variable) if variable.to_string().eq_ignore_ascii_case("NAMES") - && dialect_of!(self is MySqlDialect | GenericDialect)) + if hivevar { + self.expect_token(&Token::Colon)?; + } + + if let Some(set_role_stmt) = self.maybe_parse(|parser| parser.parse_set_role(scope))? { + return Ok(set_role_stmt); + } + + // Handle special cases first + if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) + || self.parse_keyword(Keyword::TIMEZONE) { + if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { + return Ok(Set::SingleAssignment { + scope, + hivevar, + variable: ObjectName::from(vec!["TIMEZONE".into()]), + values: self.parse_set_values(false)?, + } + .into()); + } else { + // A shorthand alias for SET TIME ZONE that doesn't require + // the assignment operator. It's originally PostgreSQL specific, + // but we allow it for all the dialects + return Ok(Set::SetTimeZone { + local: scope == Some(ContextModifier::Local), + value: self.parse_expr()?, + } + .into()); + } + } else if self.dialect.supports_set_names() && self.parse_keyword(Keyword::NAMES) { if self.parse_keyword(Keyword::DEFAULT) { - return Ok(Statement::SetNamesDefault {}); + return Ok(Set::SetNamesDefault {}.into()); } - - let charset_name = self.parse_literal_string()?; + let charset_name = self.parse_identifier()?; let collation_name = if self.parse_one_of_keywords(&[Keyword::COLLATE]).is_some() { Some(self.parse_literal_string()?) } else { None }; - return Ok(Statement::SetNames { + return Ok(Set::SetNames { charset_name, collation_name, - }); - } - - let parenthesized_assignment = matches!(&variables, OneOrManyWithParens::Many(_)); - - if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { - if parenthesized_assignment { - self.expect_token(&Token::LParen)?; - } - - let mut values = vec![]; - loop { - let value = if let Some(expr) = self.try_parse_expr_sub_query()? { - expr - } else if let Ok(expr) = self.parse_expr() { - expr - } else { - self.expected("variable value", self.peek_token())? - }; - - values.push(value); - if self.consume_token(&Token::Comma) { - continue; - } - - if parenthesized_assignment { - self.expect_token(&Token::RParen)?; - } - return Ok(Statement::SetVariable { - local: modifier == Some(Keyword::LOCAL), - hivevar: Some(Keyword::HIVEVAR) == modifier, - variables, - value: values, - }); - } - } - - let OneOrManyWithParens::One(variable) = variables else { - return self.expected("set variable", self.peek_token()); - }; - - if variable.to_string().eq_ignore_ascii_case("TIMEZONE") { - // for some db (e.g. postgresql), SET TIME ZONE is an alias for SET TIMEZONE [TO|=] - match self.parse_expr() { - Ok(expr) => Ok(Statement::SetTimeZone { - local: modifier == Some(Keyword::LOCAL), - value: expr, - }), - _ => self.expected("timezone value", self.peek_token())?, } - } else if variable.to_string() == "CHARACTERISTICS" { + .into()); + } else if self.parse_keyword(Keyword::CHARACTERISTICS) { self.expect_keywords(&[Keyword::AS, Keyword::TRANSACTION])?; - Ok(Statement::SetTransaction { + return Ok(Set::SetTransaction { modes: self.parse_transaction_modes()?, snapshot: None, session: true, - }) - } else if variable.to_string() == "TRANSACTION" && modifier.is_none() { + } + .into()); + } else if self.parse_keyword(Keyword::TRANSACTION) { if self.parse_keyword(Keyword::SNAPSHOT) { - let snapshot_id = self.parse_value()?; - return Ok(Statement::SetTransaction { + let snapshot_id = self.parse_value()?.value; + return Ok(Set::SetTransaction { modes: vec![], snapshot: Some(snapshot_id), session: false, - }); + } + .into()); } - Ok(Statement::SetTransaction { + return Ok(Set::SetTransaction { modes: self.parse_transaction_modes()?, snapshot: None, session: false, + } + .into()); + } else if self.parse_keyword(Keyword::AUTHORIZATION) { + let auth_value = if self.parse_keyword(Keyword::DEFAULT) { + SetSessionAuthorizationParamKind::Default + } else { + let value = self.parse_identifier()?; + SetSessionAuthorizationParamKind::User(value) + }; + return Ok(Set::SetSessionAuthorization(SetSessionAuthorizationParam { + scope: scope.expect("SET ... AUTHORIZATION must have a scope"), + kind: auth_value, }) - } else if self.dialect.supports_set_stmt_without_operator() { - self.prev_token(); - self.parse_set_session_params() + .into()); + } + + if self.dialect.supports_comma_separated_set_assignments() { + if scope.is_some() { + self.prev_token(); + } + + if let Some(assignments) = self + .maybe_parse(|parser| parser.parse_comma_separated(Parser::parse_set_assignment))? + { + return if assignments.len() > 1 { + Ok(Set::MultipleAssignments { assignments }.into()) + } else { + let SetAssignment { scope, name, value } = + assignments.into_iter().next().ok_or_else(|| { + ParserError::ParserError("Expected at least one assignment".to_string()) + })?; + + Ok(Set::SingleAssignment { + scope, + hivevar, + variable: name, + values: vec![value], + } + .into()) + }; + } + } + + let variables = if self.dialect.supports_parenthesized_set_variables() + && self.consume_token(&Token::LParen) + { + let vars = OneOrManyWithParens::Many( + self.parse_comma_separated(|parser: &mut Parser<'a>| parser.parse_identifier())? + .into_iter() + .map(|ident| ObjectName::from(vec![ident])) + .collect(), + ); + self.expect_token(&Token::RParen)?; + vars } else { - self.expected("equals sign or TO", self.peek_token()) + OneOrManyWithParens::One(self.parse_object_name(false)?) + }; + + if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { + let stmt = match variables { + OneOrManyWithParens::One(var) => Set::SingleAssignment { + scope, + hivevar, + variable: var, + values: self.parse_set_values(false)?, + }, + OneOrManyWithParens::Many(vars) => Set::ParenthesizedAssignments { + variables: vars, + values: self.parse_set_values(true)?, + }, + }; + + return Ok(stmt.into()); } + + if self.dialect.supports_set_stmt_without_operator() { + self.prev_token(); + return self.parse_set_session_params(); + }; + + self.expected("equals sign or TO", self.peek_token()) } pub fn parse_set_session_params(&mut self) -> Result { @@ -10471,15 +13208,20 @@ impl<'a> Parser<'a> { _ => return self.expected("IO, PROFILE, TIME or XML", self.peek_token()), }; let value = self.parse_session_param_value()?; - Ok(Statement::SetSessionParam(SetSessionParamKind::Statistics( - SetSessionParamStatistics { topic, value }, - ))) + Ok( + Set::SetSessionParam(SetSessionParamKind::Statistics(SetSessionParamStatistics { + topic, + value, + })) + .into(), + ) } else if self.parse_keyword(Keyword::IDENTITY_INSERT) { let obj = self.parse_object_name(false)?; let value = self.parse_session_param_value()?; - Ok(Statement::SetSessionParam( - SetSessionParamKind::IdentityInsert(SetSessionParamIdentityInsert { obj, value }), + Ok(Set::SetSessionParam(SetSessionParamKind::IdentityInsert( + SetSessionParamIdentityInsert { obj, value }, )) + .into()) } else if self.parse_keyword(Keyword::OFFSETS) { let keywords = self.parse_comma_separated(|parser| { let next_token = parser.next_token(); @@ -10489,9 +13231,13 @@ impl<'a> Parser<'a> { } })?; let value = self.parse_session_param_value()?; - Ok(Statement::SetSessionParam(SetSessionParamKind::Offsets( - SetSessionParamOffsets { keywords, value }, - ))) + Ok( + Set::SetSessionParam(SetSessionParamKind::Offsets(SetSessionParamOffsets { + keywords, + value, + })) + .into(), + ) } else { let names = self.parse_comma_separated(|parser| { let next_token = parser.next_token(); @@ -10501,9 +13247,13 @@ impl<'a> Parser<'a> { } })?; let value = self.parse_expr()?.to_string(); - Ok(Statement::SetSessionParam(SetSessionParamKind::Generic( - SetSessionParamGeneric { names, value }, - ))) + Ok( + Set::SetSessionParam(SetSessionParamKind::Generic(SetSessionParamGeneric { + names, + value, + })) + .into(), + ) } } @@ -10565,6 +13315,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()?, @@ -10572,6 +13326,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()?; @@ -10751,20 +13513,34 @@ impl<'a> Parser<'a> { // Note that for keywords to be properly handled here, they need to be // added to `RESERVED_FOR_TABLE_ALIAS`, otherwise they may be parsed as // a table alias. + let joins = self.parse_joins()?; + Ok(TableWithJoins { relation, joins }) + } + + fn parse_joins(&mut self) -> Result, ParserError> { let mut joins = vec![]; loop { let global = self.parse_keyword(Keyword::GLOBAL); let join = if self.parse_keyword(Keyword::CROSS) { let join_operator = if self.parse_keyword(Keyword::JOIN) { - JoinOperator::CrossJoin + JoinOperator::CrossJoin(JoinConstraint::None) } else if self.parse_keyword(Keyword::APPLY) { // MSSQL extension, similar to CROSS JOIN LATERAL JoinOperator::CrossApply } else { return self.expected("JOIN or APPLY after CROSS", self.peek_token()); }; + let relation = self.parse_table_factor()?; + let join_operator = if matches!(join_operator, JoinOperator::CrossJoin(_)) + && self.dialect.supports_cross_join_constraint() + { + let constraint = self.parse_join_constraint(false)?; + JoinOperator::CrossJoin(constraint) + } else { + join_operator + }; Join { - relation: self.parse_table_factor()?, + relation, global, join_operator, } @@ -10799,9 +13575,13 @@ impl<'a> Parser<'a> { let join_operator_type = match peek_keyword { Keyword::INNER | Keyword::JOIN => { - let _ = self.parse_keyword(Keyword::INNER); // [ INNER ] + let inner = self.parse_keyword(Keyword::INNER); // [ INNER ] self.expect_keyword_is(Keyword::JOIN)?; - JoinOperator::Inner + if inner { + JoinOperator::Inner + } else { + JoinOperator::Join + } } kw @ Keyword::LEFT | kw @ Keyword::RIGHT => { let _ = self.next_token(); // consume LEFT/RIGHT @@ -10839,9 +13619,9 @@ impl<'a> Parser<'a> { } Some(Keyword::JOIN) => { if is_left { - JoinOperator::LeftOuter + JoinOperator::Left } else { - JoinOperator::RightOuter + JoinOperator::Right } } _ => { @@ -10870,12 +13650,29 @@ impl<'a> Parser<'a> { Keyword::OUTER => { return self.expected("LEFT, RIGHT, or FULL", self.peek_token()); } + Keyword::STRAIGHT_JOIN => { + let _ = self.next_token(); // consume STRAIGHT_JOIN + JoinOperator::StraightJoin + } _ if natural => { return self.expected("a join type after NATURAL", self.peek_token()); } _ => break, }; - let relation = self.parse_table_factor()?; + let mut relation = self.parse_table_factor()?; + + 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 }), + alias: None, + }; + } + let join_constraint = self.parse_join_constraint(natural)?; Join { relation, @@ -10885,7 +13682,21 @@ impl<'a> Parser<'a> { }; joins.push(join); } - Ok(TableWithJoins { relation, joins }) + Ok(joins) + } + + fn peek_parens_less_nested_join(&self) -> bool { + matches!( + self.peek_token_ref().token, + Token::Word(Word { + keyword: Keyword::JOIN + | Keyword::INNER + | Keyword::LEFT + | Keyword::RIGHT + | Keyword::FULL, + .. + }) + ) } /// A table name or a parenthesized subquery, followed by optional `[AS] alias` @@ -10898,7 +13709,7 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(false)?; self.expect_token(&Token::LParen)?; let args = self.parse_optional_args()?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Function { lateral: true, name, @@ -10911,7 +13722,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let expr = self.parse_expr()?; self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::TableFunction { expr, alias }) } else if self.consume_token(&Token::LParen) { // A left paren introduces either a derived table (i.e., a subquery) @@ -10960,7 +13771,7 @@ impl<'a> Parser<'a> { #[allow(clippy::if_same_then_else)] if !table_and_joins.joins.is_empty() { self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::NestedJoin { table_with_joins: Box::new(table_and_joins), alias, @@ -10973,7 +13784,7 @@ impl<'a> Parser<'a> { // (B): `table_and_joins` (what we found inside the parentheses) // is a nested join `(foo JOIN bar)`, not followed by other joins. self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::NestedJoin { table_with_joins: Box::new(table_and_joins), alias, @@ -10987,9 +13798,7 @@ impl<'a> Parser<'a> { // [AS alias])`) as well. self.expect_token(&Token::RParen)?; - if let Some(outer_alias) = - self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)? - { + if let Some(outer_alias) = self.maybe_parse_table_alias()? { // Snowflake also allows specifying an alias *after* parens // e.g. `FROM (mytable) AS alias` match &mut table_and_joins.relation { @@ -10998,11 +13807,13 @@ impl<'a> Parser<'a> { | TableFactor::Function { alias, .. } | TableFactor::UNNEST { alias, .. } | TableFactor::JsonTable { alias, .. } + | TableFactor::XmlTable { alias, .. } | TableFactor::OpenJsonTable { alias, .. } | TableFactor::TableFunction { alias, .. } | 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 { @@ -11041,22 +13852,21 @@ impl<'a> Parser<'a> { // Snowflake and Databricks allow syntax like below: // SELECT * FROM VALUES (1, 'a'), (2, 'b') AS t (col1, col2) // where there are no parentheses around the VALUES clause. - let values = SetExpr::Values(self.parse_values(false)?); - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let values = SetExpr::Values(self.parse_values(false, false)?); + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Derived { lateral: false, subquery: Box::new(Query { with: None, body: Box::new(values), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), alias, }) @@ -11068,7 +13878,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; let with_ordinality = self.parse_keywords(&[Keyword::WITH, Keyword::ORDINALITY]); - let alias = match self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS) { + let alias = match self.maybe_parse_table_alias() { Ok(Some(alias)) => Some(alias), Ok(None) => None, Err(e) => return Err(e), @@ -11099,13 +13909,13 @@ impl<'a> Parser<'a> { } else if self.parse_keyword_with_tokens(Keyword::JSON_TABLE, &[Token::LParen]) { let json_expr = self.parse_expr()?; self.expect_token(&Token::Comma)?; - let json_path = self.parse_value()?; + let json_path = self.parse_value()?.value; self.expect_keyword_is(Keyword::COLUMNS)?; self.expect_token(&Token::LParen)?; let columns = self.parse_comma_separated(Parser::parse_json_table_column_def)?; self.expect_token(&Token::RParen)?; self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::JsonTable { json_expr, json_path, @@ -11115,6 +13925,13 @@ impl<'a> Parser<'a> { } else if self.parse_keyword_with_tokens(Keyword::OPENJSON, &[Token::LParen]) { self.prev_token(); self.parse_open_json_table_factor() + } 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)?; @@ -11132,7 +13949,7 @@ impl<'a> Parser<'a> { }; // Parse potential version qualifier - let version = self.parse_table_version()?; + let version = self.maybe_parse_table_version()?; // Postgres, MSSQL, ClickHouse: table-valued functions: let args = if self.consume_token(&Token::LParen) { @@ -11150,7 +13967,15 @@ impl<'a> Parser<'a> { } } - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; + + // MYSQL-specific table hints: + let index_hints = if self.dialect.supports_table_hints() { + self.maybe_parse(|p| p.parse_table_index_hints())? + .unwrap_or(vec![]) + } else { + vec![] + }; // MSSQL-specific table hints: let mut with_hints = vec![]; @@ -11180,6 +14005,7 @@ impl<'a> Parser<'a> { with_ordinality, json_path, sample, + index_hints, }; while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { @@ -11208,7 +14034,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, @@ -11225,9 +14057,9 @@ impl<'a> Parser<'a> { let parenthesized = self.consume_token(&Token::LParen); let (quantity, bucket) = if parenthesized && self.parse_keyword(Keyword::BUCKET) { - let selected_bucket = self.parse_number_value()?; + let selected_bucket = self.parse_number_value()?.value; self.expect_keywords(&[Keyword::OUT, Keyword::OF])?; - let total = self.parse_number_value()?; + let total = self.parse_number_value()?.value; let on = if self.parse_keyword(Keyword::ON) { Some(self.parse_expr()?) } else { @@ -11245,8 +14077,9 @@ impl<'a> Parser<'a> { let value = match self.maybe_parse(|p| p.parse_expr())? { Some(num) => num, None => { - if let Token::Word(w) = self.next_token().token { - Expr::Value(Value::Placeholder(w.value)) + let next_token = self.next_token(); + if let Token::Word(w) = next_token.token { + Expr::Value(Value::Placeholder(w.value).with_span(next_token.span)) } else { return parser_err!( "Expecting number or byte length e.g. 100M", @@ -11289,14 +14122,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( @@ -11304,7 +14137,7 @@ impl<'a> Parser<'a> { modifier: TableSampleSeedModifier, ) -> Result { self.expect_token(&Token::LParen)?; - let value = self.parse_number_value()?; + let value = self.parse_number_value()?.value; self.expect_token(&Token::RParen)?; Ok(TableSampleSeed { modifier, value }) } @@ -11315,7 +14148,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let json_expr = self.parse_expr()?; let json_path = if self.consume_token(&Token::Comma) { - Some(self.parse_value()?) + Some(self.parse_value()?.value) } else { None }; @@ -11328,7 +14161,7 @@ impl<'a> Parser<'a> { } else { Vec::new() }; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::OpenJsonTable { json_expr, json_path, @@ -11337,6 +14170,166 @@ impl<'a> Parser<'a> { }) } + fn parse_xml_table_factor(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let namespaces = if self.parse_keyword(Keyword::XMLNAMESPACES) { + self.expect_token(&Token::LParen)?; + let namespaces = self.parse_comma_separated(Parser::parse_xml_namespace_definition)?; + self.expect_token(&Token::RParen)?; + self.expect_token(&Token::Comma)?; + namespaces + } else { + vec![] + }; + let row_expression = self.parse_expr()?; + let passing = self.parse_xml_passing_clause()?; + self.expect_keyword_is(Keyword::COLUMNS)?; + let columns = self.parse_comma_separated(Parser::parse_xml_table_column)?; + self.expect_token(&Token::RParen)?; + let alias = self.maybe_parse_table_alias()?; + Ok(TableFactor::XmlTable { + namespaces, + row_expression, + passing, + columns, + alias, + }) + } + + fn parse_xml_namespace_definition(&mut self) -> Result { + let uri = self.parse_expr()?; + self.expect_keyword_is(Keyword::AS)?; + let name = self.parse_identifier()?; + Ok(XmlNamespaceDefinition { uri, name }) + } + + fn parse_xml_table_column(&mut self) -> Result { + let name = self.parse_identifier()?; + + let option = if self.parse_keyword(Keyword::FOR) { + self.expect_keyword(Keyword::ORDINALITY)?; + XmlTableColumnOption::ForOrdinality + } else { + let r#type = self.parse_data_type()?; + let mut path = None; + let mut default = None; + + if self.parse_keyword(Keyword::PATH) { + path = Some(self.parse_expr()?); + } + + if self.parse_keyword(Keyword::DEFAULT) { + default = Some(self.parse_expr()?); + } + + let not_null = self.parse_keywords(&[Keyword::NOT, Keyword::NULL]); + if !not_null { + // NULL is the default but can be specified explicitly + let _ = self.parse_keyword(Keyword::NULL); + } + + XmlTableColumnOption::NamedInfo { + r#type, + path, + default, + nullable: !not_null, + } + }; + Ok(XmlTableColumn { name, option }) + } + + fn parse_xml_passing_clause(&mut self) -> Result { + let mut arguments = vec![]; + if self.parse_keyword(Keyword::PASSING) { + loop { + let by_value = + self.parse_keyword(Keyword::BY) && self.expect_keyword(Keyword::VALUE).is_ok(); + let expr = self.parse_expr()?; + let alias = if self.parse_keyword(Keyword::AS) { + Some(self.parse_identifier()?) + } else { + None + }; + arguments.push(XmlPassingArgument { + expr, + alias, + by_value, + }); + if !self.consume_token(&Token::Comma) { + break; + } + } + } + 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_wildcard_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::parse_wildcard_expr)?; + } 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_wildcard_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!( + format!( + "Expected one of DIMENSIONS, METRICS, FACTS or WHERE, got {}", + self.peek_token().token + ), + 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)?; @@ -11427,7 +14420,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::MatchRecognize { table: Box::new(table), @@ -11563,18 +14556,20 @@ impl<'a> Parser<'a> { } } - /// Parse a given table version specifier. - /// - /// For now it only supports timestamp versioning for BigQuery and MSSQL dialects. - pub fn parse_table_version(&mut self) -> Result, ParserError> { - if dialect_of!(self is BigQueryDialect | MsSqlDialect) - && self.parse_keywords(&[Keyword::FOR, Keyword::SYSTEM_TIME, Keyword::AS, Keyword::OF]) - { - let expr = self.parse_expr()?; - Ok(Some(TableVersion::ForSystemTimeAsOf(expr))) - } else { - Ok(None) + /// Parses a the timestamp version specifier (i.e. query historical data) + pub fn maybe_parse_table_version(&mut self) -> Result, ParserError> { + if self.dialect.supports_timestamp_versioning() { + if self.parse_keywords(&[Keyword::FOR, Keyword::SYSTEM_TIME, Keyword::AS, Keyword::OF]) + { + let expr = self.parse_expr()?; + return Ok(Some(TableVersion::ForSystemTimeAsOf(expr))); + } else if self.peek_keyword(Keyword::AT) || self.peek_keyword(Keyword::BEFORE) { + let func_name = self.parse_object_name(true)?; + let func = self.parse_function(func_name)?; + return Ok(Some(TableVersion::Function(func))); + } } + Ok(None) } /// Parses MySQL's JSON_TABLE column definition. @@ -11582,7 +14577,7 @@ impl<'a> Parser<'a> { pub fn parse_json_table_column_def(&mut self) -> Result { if self.parse_keyword(Keyword::NESTED) { let _has_path_keyword = self.parse_keyword(Keyword::PATH); - let path = self.parse_value()?; + let path = self.parse_value()?.value; self.expect_keyword_is(Keyword::COLUMNS)?; let columns = self.parse_parenthesized(|p| { p.parse_comma_separated(Self::parse_json_table_column_def) @@ -11600,7 +14595,7 @@ impl<'a> Parser<'a> { let r#type = self.parse_data_type()?; let exists = self.parse_keyword(Keyword::EXISTS); self.expect_keyword_is(Keyword::PATH)?; - let path = self.parse_value()?; + let path = self.parse_value()?.value; let mut on_empty = None; let mut on_error = None; while let Some(error_handling) = self.parse_json_table_column_error_handling()? { @@ -11657,7 +14652,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::ERROR) { JsonTableColumnErrorHandling::Error } else if self.parse_keyword(Keyword::DEFAULT) { - JsonTableColumnErrorHandling::Default(self.parse_value()?) + JsonTableColumnErrorHandling::Default(self.parse_value()?.value) } else { return Ok(None); }; @@ -11671,7 +14666,7 @@ impl<'a> Parser<'a> { ) -> Result { let subquery = self.parse_query()?; self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Derived { lateral: match lateral { Lateral => true, @@ -11687,7 +14682,7 @@ impl<'a> Parser<'a> { Token::Word(w) => Ok(w.value), _ => self.expected("a function identifier", self.peek_token()), }?; - let expr = self.parse_function(ObjectName(vec![Ident::new(function_name)]))?; + let expr = self.parse_function(ObjectName::from(vec![Ident::new(function_name)]))?; let alias = if self.parse_keyword(Keyword::AS) { Some(self.parse_identifier()?) } else { @@ -11736,7 +14731,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_object_name(false)?.0; + 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)?; @@ -11765,7 +14766,7 @@ impl<'a> Parser<'a> { }; self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Pivot { table: Box::new(table), aggregate_functions, @@ -11780,17 +14781,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.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Unpivot { table: Box::new(table), value, + null_inclusion, name, columns, alias, @@ -11804,7 +14817,7 @@ impl<'a> Parser<'a> { let constraint = self.parse_expr()?; Ok(JoinConstraint::On(constraint)) } else if self.parse_keyword(Keyword::USING) { - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_qualified_column_list(Mandatory, false)?; Ok(JoinConstraint::Using(columns)) } else { Ok(JoinConstraint::None) @@ -11814,7 +14827,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()?; @@ -11822,16 +14835,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, }) } @@ -11839,7 +14871,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 @@ -11856,150 +14888,512 @@ 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(), name: None, } } else { - let mut name = self.parse_grantee_name()?; - if self.consume_token(&Token::Colon) { - // Redshift supports namespace prefix for extenrnal users and groups: - // : or : - // https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-native-idp.html - let ident = self.parse_identifier()?; - if let GranteeName::ObjectName(namespace) = name { - name = GranteeName::ObjectName(ObjectName(vec![Ident::new(format!( - "{}:{}", - namespace, ident - ))])); - }; - } - Grantee { - grantee_type: grantee_type.clone(), - name: Some(name), + let mut name = self.parse_grantee_name()?; + if self.consume_token(&Token::Colon) { + // Redshift supports namespace prefix for external users and groups: + // : or : + // https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-native-idp.html + let ident = self.parse_identifier()?; + if let GranteeName::ObjectName(namespace) = name { + name = GranteeName::ObjectName(ObjectName::from(vec![Ident::new( + format!("{namespace}:{ident}"), + )])); + }; + } + Grantee { + grantee_type: grantee_type.clone(), + name: Some(name), + } + }; + + values.push(grantee); + + if !self.consume_token(&Token::Comma) { + break; + } + } + + Ok(values) + } + + pub fn parse_grant_deny_revoke_privileges_objects( + &mut self, + ) -> Result<(Privileges, Option), ParserError> { + let privileges = if self.parse_keyword(Keyword::ALL) { + Privileges::All { + with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), + } + } else { + let actions = self.parse_actions_list()?; + Privileges::Actions(actions) + }; + + let objects = if self.parse_keyword(Keyword::ON) { + if self.parse_keywords(&[Keyword::ALL, Keyword::TABLES, Keyword::IN, Keyword::SCHEMA]) { + 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, + Keyword::IN, + Keyword::SCHEMA, + ]) { + 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(false))?, + )) + } else if self.parse_keywords(&[Keyword::COMPUTE, Keyword::POOL]) { + 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(false))?, + )) + } else if self.parse_keywords(&[Keyword::REPLICATION, Keyword::GROUP]) { + 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(false))?, + )) + } else { + let object_type = self.parse_one_of_keywords(&[ + Keyword::SEQUENCE, + Keyword::DATABASE, + Keyword::SCHEMA, + Keyword::TABLE, + Keyword::VIEW, + Keyword::WAREHOUSE, + Keyword::INTEGRATION, + Keyword::VIEW, + Keyword::WAREHOUSE, + Keyword::INTEGRATION, + Keyword::USER, + Keyword::CONNECTION, + Keyword::PROCEDURE, + Keyword::FUNCTION, + ]); + let objects = + 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?)), + Some(Keyword::SEQUENCE) => Some(GrantObjects::Sequences(objects?)), + Some(Keyword::WAREHOUSE) => Some(GrantObjects::Warehouses(objects?)), + Some(Keyword::INTEGRATION) => Some(GrantObjects::Integrations(objects?)), + 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!(), } - }; - - values.push(grantee); - - if !self.consume_token(&Token::Comma) { - break; } - } + } else { + None + }; - Ok(values) + Ok((privileges, objects)) } - pub fn parse_grant_revoke_privileges_objects( + fn parse_grant_procedure_or_function( &mut self, - ) -> Result<(Privileges, GrantObjects), ParserError> { - let privileges = if self.parse_keyword(Keyword::ALL) { - Privileges::All { - with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), - } + 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 { - let (actions, err): (Vec<_>, Vec<_>) = self - .parse_actions_list()? - .into_iter() - .map(|(kw, columns)| match kw { - Keyword::DELETE => Ok(Action::Delete), - Keyword::INSERT => Ok(Action::Insert { columns }), - Keyword::REFERENCES => Ok(Action::References { columns }), - Keyword::SELECT => Ok(Action::Select { columns }), - Keyword::TRIGGER => Ok(Action::Trigger), - Keyword::TRUNCATE => Ok(Action::Truncate), - Keyword::UPDATE => Ok(Action::Update { columns }), - Keyword::USAGE => Ok(Action::Usage), - Keyword::CONNECT => Ok(Action::Connect), - Keyword::CREATE => Ok(Action::Create), - Keyword::EXECUTE => Ok(Action::Execute), - Keyword::TEMPORARY => Ok(Action::Temporary), - // This will cover all future added keywords to - // parse_grant_permission and unhandled in this - // match - _ => Err(kw), - }) - .partition(Result::is_ok); - - if !err.is_empty() { - let errors: Vec = err.into_iter().filter_map(|x| x.err()).collect(); - return Err(ParserError::ParserError(format!( - "INTERNAL ERROR: GRANT/REVOKE unexpected keyword(s) - {errors:?}" - ))); - } - let act = actions.into_iter().filter_map(|x| x.ok()).collect(); - Privileges::Actions(act) + vec![] }; - - self.expect_keyword_is(Keyword::ON)?; - - let objects = if self.parse_keywords(&[ - Keyword::ALL, - Keyword::TABLES, - Keyword::IN, - Keyword::SCHEMA, - ]) { - GrantObjects::AllTablesInSchema { - schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, - } + 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)?; + if columns.is_empty() { + Ok(None) + } else { + Ok(Some(columns)) + } + } + + // Multi-word privileges + if self.parse_keywords(&[Keyword::IMPORTED, Keyword::PRIVILEGES]) { + Ok(Action::ImportedPrivileges) + } else if self.parse_keywords(&[Keyword::ADD, Keyword::SEARCH, Keyword::OPTIMIZATION]) { + Ok(Action::AddSearchOptimization) + } else if self.parse_keywords(&[Keyword::ATTACH, Keyword::LISTING]) { + Ok(Action::AttachListing) + } else if self.parse_keywords(&[Keyword::ATTACH, Keyword::POLICY]) { + Ok(Action::AttachPolicy) + } else if self.parse_keywords(&[Keyword::BIND, Keyword::SERVICE, Keyword::ENDPOINT]) { + Ok(Action::BindServiceEndpoint) + } else if self.parse_keywords(&[Keyword::DATABASE, Keyword::ROLE]) { + let role = self.parse_object_name(false)?; + Ok(Action::DatabaseRole { role }) + } else if self.parse_keywords(&[Keyword::EVOLVE, Keyword::SCHEMA]) { + Ok(Action::EvolveSchema) + } else if self.parse_keywords(&[Keyword::IMPORT, Keyword::SHARE]) { + Ok(Action::ImportShare) + } else if self.parse_keywords(&[Keyword::MANAGE, Keyword::VERSIONS]) { + Ok(Action::ManageVersions) + } else if self.parse_keywords(&[Keyword::MANAGE, Keyword::RELEASES]) { + Ok(Action::ManageReleases) + } else if self.parse_keywords(&[Keyword::OVERRIDE, Keyword::SHARE, Keyword::RESTRICTIONS]) { + Ok(Action::OverrideShareRestrictions) } else if self.parse_keywords(&[ - Keyword::ALL, - Keyword::SEQUENCES, - Keyword::IN, - Keyword::SCHEMA, + Keyword::PURCHASE, + Keyword::DATA, + Keyword::EXCHANGE, + Keyword::LISTING, ]) { - GrantObjects::AllSequencesInSchema { - schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, - } - } else { - let object_type = - self.parse_one_of_keywords(&[Keyword::SEQUENCE, Keyword::SCHEMA, Keyword::TABLE]); - let objects = - self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); - match object_type { - Some(Keyword::SCHEMA) => GrantObjects::Schemas(objects?), - Some(Keyword::SEQUENCE) => GrantObjects::Sequences(objects?), - Some(Keyword::TABLE) | None => GrantObjects::Tables(objects?), - _ => unreachable!(), - } - }; + Ok(Action::PurchaseDataExchangeListing) + } else if self.parse_keywords(&[Keyword::RESOLVE, Keyword::ALL]) { + Ok(Action::ResolveAll) + } else if self.parse_keywords(&[Keyword::READ, Keyword::SESSION]) { + Ok(Action::ReadSession) + + // Single-word privileges + } else if self.parse_keyword(Keyword::APPLY) { + let apply_type = self.parse_action_apply_type()?; + Ok(Action::Apply { apply_type }) + } else if self.parse_keyword(Keyword::APPLYBUDGET) { + Ok(Action::ApplyBudget) + } else if self.parse_keyword(Keyword::AUDIT) { + Ok(Action::Audit) + } else if self.parse_keyword(Keyword::CONNECT) { + Ok(Action::Connect) + } else if self.parse_keyword(Keyword::CREATE) { + let obj_type = self.maybe_parse_action_create_object_type(); + 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 }) + } else if self.parse_keyword(Keyword::FAILOVER) { + Ok(Action::Failover) + } else if self.parse_keyword(Keyword::INSERT) { + Ok(Action::Insert { + columns: parse_columns(self)?, + }) + } else if self.parse_keyword(Keyword::MANAGE) { + let manage_type = self.parse_action_manage_type()?; + Ok(Action::Manage { manage_type }) + } else if self.parse_keyword(Keyword::MODIFY) { + let modify_type = self.parse_action_modify_type(); + Ok(Action::Modify { modify_type }) + } else if self.parse_keyword(Keyword::MONITOR) { + let monitor_type = self.parse_action_monitor_type(); + Ok(Action::Monitor { monitor_type }) + } else if self.parse_keyword(Keyword::OPERATE) { + Ok(Action::Operate) + } else if self.parse_keyword(Keyword::REFERENCES) { + Ok(Action::References { + columns: parse_columns(self)?, + }) + } else if self.parse_keyword(Keyword::READ) { + Ok(Action::Read) + } else if self.parse_keyword(Keyword::REPLICATE) { + Ok(Action::Replicate) + } else if self.parse_keyword(Keyword::ROLE) { + let role = self.parse_object_name(false)?; + Ok(Action::Role { role }) + } else if self.parse_keyword(Keyword::SELECT) { + Ok(Action::Select { + columns: parse_columns(self)?, + }) + } else if self.parse_keyword(Keyword::TEMPORARY) { + Ok(Action::Temporary) + } else if self.parse_keyword(Keyword::TRIGGER) { + Ok(Action::Trigger) + } else if self.parse_keyword(Keyword::TRUNCATE) { + Ok(Action::Truncate) + } else if self.parse_keyword(Keyword::UPDATE) { + Ok(Action::Update { + columns: parse_columns(self)?, + }) + } else if self.parse_keyword(Keyword::USAGE) { + 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())? + } + } - Ok((privileges, objects)) + fn maybe_parse_action_create_object_type(&mut self) -> Option { + // Multi-word object types + if self.parse_keywords(&[Keyword::APPLICATION, Keyword::PACKAGE]) { + Some(ActionCreateObjectType::ApplicationPackage) + } else if self.parse_keywords(&[Keyword::COMPUTE, Keyword::POOL]) { + Some(ActionCreateObjectType::ComputePool) + } else if self.parse_keywords(&[Keyword::DATA, Keyword::EXCHANGE, Keyword::LISTING]) { + Some(ActionCreateObjectType::DataExchangeListing) + } else if self.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) { + Some(ActionCreateObjectType::ExternalVolume) + } else if self.parse_keywords(&[Keyword::FAILOVER, Keyword::GROUP]) { + Some(ActionCreateObjectType::FailoverGroup) + } else if self.parse_keywords(&[Keyword::NETWORK, Keyword::POLICY]) { + Some(ActionCreateObjectType::NetworkPolicy) + } else if self.parse_keywords(&[Keyword::ORGANIZATION, Keyword::LISTING]) { + Some(ActionCreateObjectType::OrganiationListing) + } else if self.parse_keywords(&[Keyword::REPLICATION, Keyword::GROUP]) { + Some(ActionCreateObjectType::ReplicationGroup) + } + // Single-word object types + else if self.parse_keyword(Keyword::ACCOUNT) { + Some(ActionCreateObjectType::Account) + } else if self.parse_keyword(Keyword::APPLICATION) { + Some(ActionCreateObjectType::Application) + } else if self.parse_keyword(Keyword::DATABASE) { + Some(ActionCreateObjectType::Database) + } else if self.parse_keyword(Keyword::INTEGRATION) { + 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) { + Some(ActionCreateObjectType::User) + } else if self.parse_keyword(Keyword::WAREHOUSE) { + Some(ActionCreateObjectType::Warehouse) + } else { + None + } } - pub fn parse_grant_permission(&mut self) -> Result { - if let Some(kw) = self.parse_one_of_keywords(&[ - Keyword::CONNECT, - Keyword::CREATE, - Keyword::DELETE, - Keyword::EXECUTE, - Keyword::INSERT, - Keyword::REFERENCES, - Keyword::SELECT, - Keyword::TEMPORARY, - Keyword::TRIGGER, - Keyword::TRUNCATE, - Keyword::UPDATE, - Keyword::USAGE, - ]) { - let columns = match kw { - Keyword::INSERT | Keyword::REFERENCES | Keyword::SELECT | Keyword::UPDATE => { - let columns = self.parse_parenthesized_column_list(Optional, false)?; - if columns.is_empty() { - None - } else { - Some(columns) - } - } - _ => None, - }; - Ok((kw, columns)) + fn parse_action_apply_type(&mut self) -> Result { + if self.parse_keywords(&[Keyword::AGGREGATION, Keyword::POLICY]) { + Ok(ActionApplyType::AggregationPolicy) + } else if self.parse_keywords(&[Keyword::AUTHENTICATION, Keyword::POLICY]) { + Ok(ActionApplyType::AuthenticationPolicy) + } else if self.parse_keywords(&[Keyword::JOIN, Keyword::POLICY]) { + Ok(ActionApplyType::JoinPolicy) + } else if self.parse_keywords(&[Keyword::MASKING, Keyword::POLICY]) { + Ok(ActionApplyType::MaskingPolicy) + } else if self.parse_keywords(&[Keyword::PACKAGES, Keyword::POLICY]) { + Ok(ActionApplyType::PackagesPolicy) + } else if self.parse_keywords(&[Keyword::PASSWORD, Keyword::POLICY]) { + Ok(ActionApplyType::PasswordPolicy) + } else if self.parse_keywords(&[Keyword::PROJECTION, Keyword::POLICY]) { + Ok(ActionApplyType::ProjectionPolicy) + } else if self.parse_keywords(&[Keyword::ROW, Keyword::ACCESS, Keyword::POLICY]) { + Ok(ActionApplyType::RowAccessPolicy) + } else if self.parse_keywords(&[Keyword::SESSION, Keyword::POLICY]) { + Ok(ActionApplyType::SessionPolicy) + } else if self.parse_keyword(Keyword::TAG) { + Ok(ActionApplyType::Tag) + } else { + self.expected("GRANT APPLY type", self.peek_token()) + } + } + + fn maybe_parse_action_execute_obj_type(&mut self) -> Option { + if self.parse_keywords(&[Keyword::DATA, Keyword::METRIC, Keyword::FUNCTION]) { + Some(ActionExecuteObjectType::DataMetricFunction) + } else if self.parse_keywords(&[Keyword::MANAGED, Keyword::ALERT]) { + Some(ActionExecuteObjectType::ManagedAlert) + } else if self.parse_keywords(&[Keyword::MANAGED, Keyword::TASK]) { + Some(ActionExecuteObjectType::ManagedTask) + } else if self.parse_keyword(Keyword::ALERT) { + Some(ActionExecuteObjectType::Alert) + } else if self.parse_keyword(Keyword::TASK) { + Some(ActionExecuteObjectType::Task) } else { - self.expected("a privilege keyword", self.peek_token())? + None + } + } + + fn parse_action_manage_type(&mut self) -> Result { + if self.parse_keywords(&[Keyword::ACCOUNT, Keyword::SUPPORT, Keyword::CASES]) { + Ok(ActionManageType::AccountSupportCases) + } else if self.parse_keywords(&[Keyword::EVENT, Keyword::SHARING]) { + Ok(ActionManageType::EventSharing) + } else if self.parse_keywords(&[Keyword::LISTING, Keyword::AUTO, Keyword::FULFILLMENT]) { + Ok(ActionManageType::ListingAutoFulfillment) + } else if self.parse_keywords(&[Keyword::ORGANIZATION, Keyword::SUPPORT, Keyword::CASES]) { + Ok(ActionManageType::OrganizationSupportCases) + } else if self.parse_keywords(&[Keyword::USER, Keyword::SUPPORT, Keyword::CASES]) { + Ok(ActionManageType::UserSupportCases) + } else if self.parse_keyword(Keyword::GRANTS) { + Ok(ActionManageType::Grants) + } else if self.parse_keyword(Keyword::WAREHOUSES) { + Ok(ActionManageType::Warehouses) + } else { + self.expected("GRANT MANAGE type", self.peek_token()) + } + } + + fn parse_action_modify_type(&mut self) -> Option { + if self.parse_keywords(&[Keyword::LOG, Keyword::LEVEL]) { + Some(ActionModifyType::LogLevel) + } else if self.parse_keywords(&[Keyword::TRACE, Keyword::LEVEL]) { + Some(ActionModifyType::TraceLevel) + } else if self.parse_keywords(&[Keyword::SESSION, Keyword::LOG, Keyword::LEVEL]) { + Some(ActionModifyType::SessionLogLevel) + } else if self.parse_keywords(&[Keyword::SESSION, Keyword::TRACE, Keyword::LEVEL]) { + Some(ActionModifyType::SessionTraceLevel) + } else { + None + } + } + + fn parse_action_monitor_type(&mut self) -> Option { + if self.parse_keyword(Keyword::EXECUTION) { + Some(ActionMonitorType::Execution) + } else if self.parse_keyword(Keyword::SECURITY) { + Some(ActionMonitorType::Security) + } else if self.parse_keyword(Keyword::USAGE) { + Some(ActionMonitorType::Usage) + } else { + None } } @@ -12007,9 +15401,10 @@ impl<'a> Parser<'a> { let mut name = self.parse_object_name(false)?; if self.dialect.supports_user_host_grantee() && name.0.len() == 1 + && name.0[0].as_ident().is_some() && self.consume_token(&Token::AtSign) { - let user = name.0.pop().unwrap(); + let user = name.0.pop().unwrap().as_ident().unwrap().clone(); let host = self.parse_identifier()?; Ok(GranteeName::UserHost { user, host }) } else { @@ -12017,16 +15412,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(); @@ -12333,7 +15763,7 @@ impl<'a> Parser<'a> { let table = self.parse_table_and_joins()?; let from_before_set = if self.parse_keyword(Keyword::FROM) { Some(UpdateTableFromKind::BeforeSet( - self.parse_table_and_joins()?, + self.parse_table_with_joins()?, )) } else { None @@ -12341,7 +15771,9 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::SET)?; let assignments = self.parse_comma_separated(Parser::parse_assignment)?; let from = if from_before_set.is_none() && self.parse_keyword(Keyword::FROM) { - Some(UpdateTableFromKind::AfterSet(self.parse_table_and_joins()?)) + Some(UpdateTableFromKind::AfterSet( + self.parse_table_with_joins()?, + )) } else { from_before_set }; @@ -12355,14 +15787,21 @@ impl<'a> Parser<'a> { } else { None }; - Ok(Statement::Update { + let limit = if self.parse_keyword(Keyword::LIMIT) { + Some(self.parse_expr()?) + } else { + None + }; + Ok(Update { table, assignments, from, selection, returning, or, - }) + limit, + } + .into()) } /// Parse a `var = expr` assignment, used in an UPDATE statement @@ -12475,7 +15914,7 @@ impl<'a> Parser<'a> { Ok(TableFunctionArgs { args, settings }) } - /// Parses a potentially empty list of arguments to a window function + /// Parses a potentially empty list of arguments to a function /// (including the closing parenthesis). /// /// Examples: @@ -12486,11 +15925,18 @@ impl<'a> Parser<'a> { fn parse_function_argument_list(&mut self) -> Result { let mut clauses = vec![]; - // For MSSQL empty argument list with json-null-clause case, e.g. `JSON_ARRAY(NULL ON NULL)` + // Handle clauses that may exist with an empty argument list + if let Some(null_clause) = self.parse_json_null_clause() { clauses.push(FunctionArgumentClause::JsonNullClause(null_clause)); } + if let Some(json_returning_clause) = self.maybe_parse_json_returning_clause()? { + clauses.push(FunctionArgumentClause::JsonReturningClause( + json_returning_clause, + )); + } + if self.consume_token(&Token::RParen) { return Ok(FunctionArgumentList { duplicate_treatment: None, @@ -12535,7 +15981,7 @@ impl<'a> Parser<'a> { if dialect_of!(self is GenericDialect | MySqlDialect) && self.parse_keyword(Keyword::SEPARATOR) { - clauses.push(FunctionArgumentClause::Separator(self.parse_value()?)); + clauses.push(FunctionArgumentClause::Separator(self.parse_value()?.value)); } if let Some(on_overflow) = self.parse_listagg_on_overflow()? { @@ -12546,6 +15992,12 @@ impl<'a> Parser<'a> { clauses.push(FunctionArgumentClause::JsonNullClause(null_clause)); } + if let Some(json_returning_clause) = self.maybe_parse_json_returning_clause()? { + clauses.push(FunctionArgumentClause::JsonReturningClause( + json_returning_clause, + )); + } + self.expect_token(&Token::RParen)?; Ok(FunctionArgumentList { duplicate_treatment, @@ -12554,7 +16006,6 @@ impl<'a> Parser<'a> { }) } - /// Parses MSSQL's json-null-clause fn parse_json_null_clause(&mut self) -> Option { if self.parse_keywords(&[Keyword::ABSENT, Keyword::ON, Keyword::NULL]) { Some(JsonNullClause::AbsentOnNull) @@ -12565,6 +16016,17 @@ impl<'a> Parser<'a> { } } + fn maybe_parse_json_returning_clause( + &mut self, + ) -> Result, ParserError> { + if self.parse_keyword(Keyword::RETURNING) { + let data_type = self.parse_data_type()?; + Ok(Some(JsonReturningClause { data_type })) + } else { + Ok(None) + } + } + fn parse_duplicate_treatment(&mut self) -> Result, ParserError> { let loc = self.peek_token().span.start; match ( @@ -12580,9 +16042,16 @@ impl<'a> Parser<'a> { /// Parse a comma-delimited list of projections after SELECT pub fn parse_select_item(&mut self) -> Result { + let prefix = self + .parse_one_of_keywords( + self.dialect + .get_reserved_keywords_for_select_item_operator(), + ) + .map(|keyword| Ident::new(format!("{keyword:?}"))); + match self.parse_wildcard_expr()? { Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard( - prefix, + SelectItemQualifiedWildcardKind::ObjectName(prefix), self.parse_wildcard_additional_options(token.0)?, )), Expr::Wildcard(token) => Ok(SelectItem::Wildcard( @@ -12612,11 +16081,23 @@ impl<'a> Parser<'a> { alias, }) } + expr if self.dialect.supports_select_expr_star() + && self.consume_tokens(&[Token::Period, Token::Mul]) => + { + let wildcard_token = self.get_previous_token().clone(); + Ok(SelectItem::QualifiedWildcard( + SelectItemQualifiedWildcardKind::Expr(expr), + self.parse_wildcard_additional_options(wildcard_token)?, + )) + } expr => self - .parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) + .maybe_parse_select_item_alias() .map(|alias| match alias { - Some(alias) => SelectItem::ExprWithAlias { expr, alias }, - None => SelectItem::UnnamedExpr(expr), + Some(alias) => SelectItem::ExprWithAlias { + expr: maybe_prefixed_expr(expr, prefix), + alias, + }, + None => SelectItem::UnnamedExpr(maybe_prefixed_expr(expr, prefix)), }), } } @@ -12633,8 +16114,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 { @@ -12809,20 +16289,44 @@ impl<'a> Parser<'a> { } } - /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) + /// Parse an [OrderByExpr] expression. pub fn parse_order_by_expr(&mut self) -> Result { - let expr = self.parse_expr()?; + self.parse_order_by_expr_inner(false) + .map(|(order_by, _)| order_by) + } - let asc = self.parse_asc_desc(); + /// Parse an [IndexColumn]. + pub fn parse_create_index_expr(&mut self) -> Result { + self.parse_order_by_expr_inner(true) + .map(|(column, operator_class)| IndexColumn { + column, + operator_class, + }) + } - let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { - Some(true) - } else if self.parse_keywords(&[Keyword::NULLS, Keyword::LAST]) { - Some(false) + fn parse_order_by_expr_inner( + &mut self, + with_operator_class: bool, + ) -> Result<(OrderByExpr, Option), ParserError> { + let expr = self.parse_expr()?; + + let operator_class: Option = if with_operator_class { + // We check that if non of the following keywords are present, then we parse an + // identifier as operator class. + if self + .peek_one_of_keywords(&[Keyword::ASC, Keyword::DESC, Keyword::NULLS, Keyword::WITH]) + .is_some() + { + None + } else { + self.maybe_parse(|parser| parser.parse_identifier())? + } } else { None }; + let options = self.parse_order_by_options()?; + let with_fill = if dialect_of!(self is ClickHouseDialect | GenericDialect) && self.parse_keywords(&[Keyword::WITH, Keyword::FILL]) { @@ -12831,12 +16335,28 @@ impl<'a> Parser<'a> { None }; - Ok(OrderByExpr { - expr, - asc, - nulls_first, - with_fill, - }) + Ok(( + OrderByExpr { + expr, + options, + with_fill, + }, + operator_class, + )) + } + + fn parse_order_by_options(&mut self) -> Result { + let asc = self.parse_asc_desc(); + + let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { + Some(true) + } else if self.parse_keywords(&[Keyword::NULLS, Keyword::LAST]) { + Some(false) + } else { + None + }; + + Ok(OrderByOptions { asc, nulls_first }) } // Parse a WITH FILL clause (ClickHouse dialect) @@ -12863,7 +16383,7 @@ impl<'a> Parser<'a> { Ok(WithFill { from, to, step }) } - // Parse a set of comma seperated INTERPOLATE expressions (ClickHouse dialect) + // Parse a set of comma separated INTERPOLATE expressions (ClickHouse dialect) // that follow the INTERPOLATE keyword in an ORDER BY clause with the WITH FILL modifier pub fn parse_interpolations(&mut self) -> Result, ParserError> { if !self.parse_keyword(Keyword::INTERPOLATE) { @@ -12946,7 +16466,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() @@ -12955,16 +16476,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, @@ -12998,7 +16519,11 @@ impl<'a> Parser<'a> { }) } - pub fn parse_values(&mut self, allow_empty: bool) -> Result { + pub fn parse_values( + &mut self, + allow_empty: bool, + value_keyword: bool, + ) -> Result { let mut explicit_row = false; let rows = self.parse_comma_separated(|parser| { @@ -13016,7 +16541,11 @@ impl<'a> Parser<'a> { Ok(exprs) } })?; - Ok(Values { explicit_row, rows }) + Ok(Values { + explicit_row, + rows, + value_keyword, + }) } pub fn parse_start_transaction(&mut self) -> Result { @@ -13026,6 +16555,9 @@ impl<'a> Parser<'a> { begin: false, transaction: Some(BeginTransactionKind::Transaction), modifier: None, + statements: vec![], + exception: None, + has_end_keyword: false, }) } @@ -13055,6 +16587,54 @@ impl<'a> Parser<'a> { begin: true, transaction, modifier, + statements: vec![], + 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(), }) } @@ -13135,18 +16715,58 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::CHAIN)?; Ok(chain) } else { - Ok(false) - } + Ok(false) + } + } + + pub fn parse_rollback_savepoint(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::TO) { + let _ = self.parse_keyword(Keyword::SAVEPOINT); + let savepoint = self.parse_identifier()?; + + Ok(Some(savepoint)) + } else { + Ok(None) + } + } + + /// Parse a 'RAISERROR' statement + pub fn parse_raiserror(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let message = Box::new(self.parse_expr()?); + self.expect_token(&Token::Comma)?; + let severity = Box::new(self.parse_expr()?); + self.expect_token(&Token::Comma)?; + let state = Box::new(self.parse_expr()?); + let arguments = if self.consume_token(&Token::Comma) { + self.parse_comma_separated(Parser::parse_expr)? + } else { + vec![] + }; + self.expect_token(&Token::RParen)?; + let options = if self.parse_keyword(Keyword::WITH) { + self.parse_comma_separated(Parser::parse_raiserror_option)? + } else { + vec![] + }; + Ok(Statement::RaisError { + message, + severity, + state, + arguments, + options, + }) } - pub fn parse_rollback_savepoint(&mut self) -> Result, ParserError> { - if self.parse_keyword(Keyword::TO) { - let _ = self.parse_keyword(Keyword::SAVEPOINT); - let savepoint = self.parse_identifier()?; - - Ok(Some(savepoint)) - } else { - Ok(None) + pub fn parse_raiserror_option(&mut self) -> Result { + match self.expect_one_of_keywords(&[Keyword::LOG, Keyword::NOWAIT, Keyword::SETERROR])? { + Keyword::LOG => Ok(RaisErrorOption::Log), + Keyword::NOWAIT => Ok(RaisErrorOption::NoWait), + Keyword::SETERROR => Ok(RaisErrorOption::SetError), + _ => self.expected( + "LOG, NOWAIT OR SETERROR raiserror option", + self.peek_token(), + ), } } @@ -13157,14 +16777,22 @@ impl<'a> Parser<'a> { } pub fn parse_execute(&mut self) -> Result { - let name = self.parse_object_name(false)?; + let name = if self.dialect.supports_execute_immediate() + && self.parse_keyword(Keyword::IMMEDIATE) + { + None + } else { + let name = self.parse_object_name(false)?; + Some(name) + }; 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, }; @@ -13174,20 +16802,31 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; } - let mut using = vec![]; - if self.parse_keyword(Keyword::USING) { - using.push(self.parse_expr()?); + let into = if self.parse_keyword(Keyword::INTO) { + self.parse_comma_separated(Self::parse_identifier)? + } else { + vec![] + }; - while self.consume_token(&Token::Comma) { - using.push(self.parse_expr()?); - } + let using = if self.parse_keyword(Keyword::USING) { + self.parse_comma_separated(Self::parse_expr_with_alias)? + } else { + vec![] }; + let output = self.parse_keyword(Keyword::OUTPUT); + + let default = self.parse_keyword(Keyword::DEFAULT); + Ok(Statement::Execute { + immediate: name.is_none(), name, parameters, has_parentheses, + into, using, + output, + default, }) } @@ -13210,29 +16849,44 @@ impl<'a> Parser<'a> { } pub fn parse_unload(&mut self) -> Result { + self.expect_keyword(Keyword::UNLOAD)?; self.expect_token(&Token::LParen)?; - let query = self.parse_query()?; + let (query, query_text) = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) + { + (None, Some(self.parse_literal_string()?)) + } else { + (Some(self.parse_query()?), None) + }; self.expect_token(&Token::RParen)?; self.expect_keyword_is(Keyword::TO)?; let to = self.parse_identifier()?; - - let with_options = self.parse_options(Keyword::WITH)?; - + let auth = if self.parse_keyword(Keyword::IAM_ROLE) { + Some(self.parse_iam_role_kind()?) + } else { + None + }; + let with = self.parse_options(Keyword::WITH)?; + let mut options = vec![]; + while let Some(opt) = self.maybe_parse(|parser| parser.parse_copy_legacy_option())? { + options.push(opt); + } Ok(Statement::Unload { query, + query_text, to, - with: with_options, + auth, + with, + options, }) } pub fn parse_merge_clauses(&mut self) -> Result, ParserError> { let mut clauses = vec![]; loop { - if self.peek_token() == Token::EOF || self.peek_token() == Token::SemiColon { + if !(self.parse_keyword(Keyword::WHEN)) { break; } - self.expect_keyword_is(Keyword::WHEN)?; let mut clause_kind = MergeClauseKind::Matched; if self.parse_keyword(Keyword::NOT) { @@ -13306,7 +16960,7 @@ impl<'a> Parser<'a> { MergeInsertKind::Row } else { self.expect_keyword_is(Keyword::VALUES)?; - let values = self.parse_values(is_mysql)?; + let values = self.parse_values(is_mysql, false)?; MergeInsertKind::Values(values) }; MergeAction::Insert(MergeInsertExpr { columns, kind }) @@ -13326,6 +16980,41 @@ impl<'a> Parser<'a> { Ok(clauses) } + fn parse_output(&mut self, start_keyword: Keyword) -> Result { + let select_items = self.parse_projection()?; + 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(if start_keyword == Keyword::OUTPUT { + OutputClause::Output { + select_items, + into_table, + } + } else { + OutputClause::Returning { select_items } + }) + } + + fn parse_select_into(&mut self) -> Result { + let temporary = self + .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) + .is_some(); + let unlogged = self.parse_keyword(Keyword::UNLOGGED); + let table = self.parse_keyword(Keyword::TABLE); + let name = self.parse_object_name(false)?; + + Ok(SelectInto { + temporary, + unlogged, + table, + name, + }) + } + pub fn parse_merge(&mut self) -> Result { let into = self.parse_keyword(Keyword::INTO); @@ -13336,6 +17025,10 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::ON)?; let on = self.parse_expr()?; let clauses = self.parse_merge_clauses()?; + 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 { into, @@ -13343,11 +17036,12 @@ impl<'a> Parser<'a> { source, on: Box::new(on), clauses, + output, }) } fn parse_pragma_value(&mut self) -> Result { - match self.parse_value()? { + match self.parse_value()?.value { v @ Value::SingleQuotedString(_) => Ok(v), v @ Value::DoubleQuotedString(_) => Ok(v), v @ Value::Number(_, _) => Ok(v), @@ -13481,7 +17175,7 @@ impl<'a> Parser<'a> { // [ OWNED BY { table_name.column_name | NONE } ] let owned_by = if self.parse_keywords(&[Keyword::OWNED, Keyword::BY]) { if self.parse_keywords(&[Keyword::NONE]) { - Some(ObjectName(vec![Ident::new("NONE")])) + Some(ObjectName::from(vec![Ident::new("NONE")])) } else { Some(self.parse_object_name(false)?) } @@ -13543,6 +17237,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 @@ -13566,22 +17303,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, }; @@ -13616,6 +17361,10 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(false)?; self.expect_keyword_is(Keyword::AS)?; + if self.parse_keyword(Keyword::ENUM) { + return self.parse_create_type_enum(name); + } + let mut attributes = vec![]; if !self.consume_token(&Token::LParen) || self.consume_token(&Token::RParen) { return Ok(Statement::CreateType { @@ -13652,11 +17401,25 @@ impl<'a> Parser<'a> { }) } + /// Parse remainder of `CREATE TYPE AS ENUM` statement (see [Statement::CreateType] and [Self::parse_create_type]) + /// + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtype.html) + pub fn parse_create_type_enum(&mut self, name: ObjectName) -> Result { + self.expect_token(&Token::LParen)?; + let labels = self.parse_comma_separated0(|p| p.parse_identifier(), Token::RParen)?; + self.expect_token(&Token::RParen)?; + + Ok(Statement::CreateType { + name, + representation: UserDefinedTypeRepresentation::Enum { labels }, + }) + } + 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> { @@ -13674,6 +17437,81 @@ impl<'a> Parser<'a> { } } + /// Parse [Statement::Print] + fn parse_print(&mut self) -> Result { + Ok(Statement::Print(PrintStatement { + message: Box::new(self.parse_expr()?), + })) + } + + /// Parse [Statement::Return] + fn parse_return(&mut self) -> Result { + match self.maybe_parse(|p| p.parse_expr())? { + Some(expr) => Ok(Statement::Return(ReturnStatement { + value: Some(ReturnStatementValue::Expr(expr)), + })), + None => Ok(Statement::Return(ReturnStatement { value: None })), + } + } + + /// /// 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 @@ -13691,7 +17529,7 @@ impl<'a> Parser<'a> { false } - fn parse_show_stmt_options(&mut self) -> Result { + pub(crate) fn parse_show_stmt_options(&mut self) -> Result { let show_in; let mut filter_position = None; if self.dialect.supports_show_like_before_in() { @@ -13772,7 +17610,9 @@ impl<'a> Parser<'a> { .parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) .is_some() { - parent_name.0.insert(0, self.parse_identifier()?); + parent_name + .0 + .insert(0, ObjectNamePart::Identifier(self.parse_identifier()?)); } (None, Some(parent_name)) } @@ -13787,7 +17627,7 @@ impl<'a> Parser<'a> { fn maybe_parse_show_stmt_starts_with(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::STARTS, Keyword::WITH]) { - Ok(Some(self.parse_value()?)) + Ok(Some(self.parse_value()?.value)) } else { Ok(None) } @@ -13803,11 +17643,141 @@ impl<'a> Parser<'a> { fn maybe_parse_show_stmt_from(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::FROM) { - Ok(Some(self.parse_value()?)) + Ok(Some(self.parse_value()?.value)) } else { 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 { + let mut options: Vec = Vec::new(); + let mut delimiter = KeyValueOptionsDelimiter::Space; + 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 => { + delimiter = KeyValueOptionsDelimiter::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(KeyValueOptions { delimiter, 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.peek_token().token { + Token::SingleQuotedString(_) => Ok(KeyValueOption { + option_name: key.value.clone(), + option_value: KeyValueOptionKind::Single(self.parse_value()?.into()), + }), + Token::Word(word) + if word.keyword == Keyword::TRUE || word.keyword == Keyword::FALSE => + { + Ok(KeyValueOption { + option_name: key.value.clone(), + option_value: KeyValueOptionKind::Single(self.parse_value()?.into()), + }) + } + Token::Number(..) => Ok(KeyValueOption { + option_name: key.value.clone(), + option_value: KeyValueOptionKind::Single(self.parse_value()?.into()), + }), + Token::Word(word) => { + self.next_token(); + Ok(KeyValueOption { + option_name: key.value.clone(), + option_value: KeyValueOptionKind::Single(Value::Placeholder( + word.value.clone(), + )), + }) + } + Token::LParen => { + // Can be a list of values or a list of key value properties. + // Try to parse a list of values and if that fails, try to parse + // a list of key-value properties. + match self.maybe_parse(|parser| { + parser.expect_token(&Token::LParen)?; + let values = parser.parse_comma_separated0(|p| p.parse_value(), Token::RParen); + parser.expect_token(&Token::RParen)?; + values + })? { + Some(values) => { + let values = values.into_iter().map(|v| v.value).collect(); + Ok(KeyValueOption { + option_name: key.value.clone(), + option_value: KeyValueOptionKind::Multi(values), + }) + } + None => Ok(KeyValueOption { + option_name: key.value.clone(), + option_value: KeyValueOptionKind::KeyValueOptions(Box::new( + self.parse_key_value_options(true, &[])?, + )), + }), + } + } + _ => self.expected("expected option value", self.peek_token()), + } + } + + /// Parses a RESET statement + fn parse_reset(&mut self) -> Result { + if self.parse_keyword(Keyword::ALL) { + return Ok(Statement::Reset(ResetStatement { reset: Reset::ALL })); + } + + let obj = self.parse_object_name(false)?; + Ok(Statement::Reset(ResetStatement { + reset: Reset::ConfigurationParameter(obj), + })) + } +} + +fn maybe_prefixed_expr(expr: Expr, prefix: Option) -> Expr { + if let Some(prefix) = prefix { + Expr::Prefixed { + prefix, + value: Box::new(expr), + } + } else { + expr + } } impl Word { @@ -13907,7 +17877,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 { @@ -14088,14 +18058,14 @@ mod tests { test_parse_data_type!( dialect, "GEOMETRY", - DataType::Custom(ObjectName(vec!["GEOMETRY".into()]), vec![]) + DataType::Custom(ObjectName::from(vec!["GEOMETRY".into()]), vec![]) ); test_parse_data_type!( dialect, "GEOMETRY(POINT)", DataType::Custom( - ObjectName(vec!["GEOMETRY".into()]), + ObjectName::from(vec!["GEOMETRY".into()]), vec!["POINT".to_string()] ) ); @@ -14104,7 +18074,7 @@ mod tests { dialect, "GEOMETRY(POINT, 4326)", DataType::Custom( - ObjectName(vec!["GEOMETRY".into()]), + ObjectName::from(vec!["GEOMETRY".into()]), vec!["POINT".to_string(), "4326".to_string()] ) ); @@ -14113,8 +18083,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)); @@ -14157,6 +18130,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] @@ -14240,7 +18260,7 @@ mod tests { }}; } - let dummy_name = ObjectName(vec![Ident::new("dummy_name")]); + let dummy_name = ObjectName::from(vec![Ident::new("dummy_name")]); let dummy_authorization = Ident::new("dummy_authorization"); test_parse_schema_name!( @@ -14272,84 +18292,111 @@ 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 {})]); test_parse_table_constraint!( dialect, "INDEX (c1)", - TableConstraint::Index { + IndexConstraint { display_as_key: false, name: None, index_type: None, - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], + index_options: vec![], } + .into() ); test_parse_table_constraint!( dialect, "KEY (c1)", - TableConstraint::Index { + IndexConstraint { display_as_key: true, name: None, index_type: None, - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], + index_options: vec![], } + .into() ); test_parse_table_constraint!( dialect, "INDEX 'index' (c1, c2)", - TableConstraint::Index { + TableConstraint::Index(IndexConstraint { 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![], + }) ); test_parse_table_constraint!( dialect, "INDEX USING BTREE (c1)", - TableConstraint::Index { + IndexConstraint { 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![], } + .into() ); test_parse_table_constraint!( dialect, "INDEX USING HASH (c1)", - TableConstraint::Index { + IndexConstraint { 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![], } + .into() ); test_parse_table_constraint!( dialect, "INDEX idx_name USING BTREE (c1)", - TableConstraint::Index { + IndexConstraint { 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![], } + .into() ); test_parse_table_constraint!( dialect, "INDEX idx_name USING HASH (c1)", - TableConstraint::Index { + IndexConstraint { 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![], } + .into() ); } @@ -14372,7 +18419,7 @@ mod tests { assert_eq!( ast, Err(ParserError::ParserError( - "Expected: [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: a at Line: 1, Column: 16" + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: a at Line: 1, Column: 16" .to_string() )) ); @@ -14518,4 +18565,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 914be7d9f..b6100d498 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -33,7 +33,7 @@ use core::fmt::Debug; use crate::dialect::*; use crate::parser::{Parser, ParserError}; -use crate::tokenizer::Tokenizer; +use crate::tokenizer::{Token, Tokenizer}; use crate::{ast::*, parser::ParserOptions}; #[cfg(test)] @@ -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 { @@ -237,6 +257,22 @@ impl TestedDialects { pub fn verified_expr(&self, sql: &str) -> Expr { self.expr_parses_to(sql, sql) } + + /// Check that the tokenizer returns the expected tokens for the given SQL. + pub fn tokenizes_to(&self, sql: &str, expected: Vec) { + if self.dialects.is_empty() { + panic!("No dialects to test"); + } + + self.dialects.iter().for_each(|dialect| { + let mut tokenizer = Tokenizer::new(&**dialect, sql); + if let Some(options) = &self.options { + tokenizer = tokenizer.with_unescape(options.unescape); + } + let tokens = tokenizer.tokenize().unwrap(); + assert_eq!(expected, tokens, "Tokenized differently for {dialect:?}"); + }); + } } /// Returns all available dialects. @@ -258,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 @@ -302,18 +343,12 @@ pub fn expr_from_projection(item: &SelectItem) -> &Expr { pub fn alter_table_op_with_name(stmt: Statement, expected_name: &str) -> AlterTableOperation { match stmt { - Statement::AlterTable { - name, - if_exists, - only: is_only, - operations, - on_cluster: _, - location: _, - } => { - assert_eq!(name.to_string(), expected_name); - assert!(!if_exists); - assert!(!is_only); - only(operations) + Statement::AlterTable(alter_table) => { + assert_eq!(alter_table.name.to_string(), expected_name); + assert!(!alter_table.if_exists); + assert!(!alter_table.only); + assert_eq!(alter_table.table_type, None); + only(alter_table.operations) } _ => panic!("Expected ALTER TABLE statement"), } @@ -328,6 +363,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), @@ -337,7 +377,7 @@ pub fn table_alias(name: impl Into) -> Option { pub fn table(name: impl Into) -> TableFactor { TableFactor::Table { - name: ObjectName(vec![Ident::new(name.into())]), + name: ObjectName::from(vec![Ident::new(name.into())]), alias: None, args: None, with_hints: vec![], @@ -346,6 +386,7 @@ pub fn table(name: impl Into) -> TableFactor { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], } } @@ -360,12 +401,13 @@ pub fn table_from_name(name: ObjectName) -> TableFactor { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], } } pub fn table_with_alias(name: impl Into, alias: impl Into) -> TableFactor { TableFactor::Table { - name: ObjectName(vec![Ident::new(name)]), + name: ObjectName::from(vec![Ident::new(name)]), alias: Some(TableAlias { name: Ident::new(alias), columns: vec![], @@ -377,6 +419,7 @@ pub fn table_with_alias(name: impl Into, alias: impl Into) -> Ta with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], } } @@ -384,13 +427,13 @@ pub fn join(relation: TableFactor) -> Join { Join { relation, global: false, - join_operator: JoinOperator::Inner(JoinConstraint::Natural), + join_operator: JoinOperator::Join(JoinConstraint::Natural), } } pub fn call(function: &str, args: impl IntoIterator) -> Expr { Expr::Function(Function { - name: ObjectName(vec![Ident::new(function)]), + name: ObjectName::from(vec![Ident::new(function)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -407,3 +450,51 @@ 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(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() + } + TableConstraint::Unique(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() + } + TableConstraint::PrimaryKey(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() + } + TableConstraint::FulltextOrSpatial(constraint) => { + constraint.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(alter_table) => match alter_table.operations.first().unwrap() { + AlterTableOperation::AddConstraint { constraint, .. } => { + match constraint { + TableConstraint::Index(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() + } + TableConstraint::Unique(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() + } + TableConstraint::PrimaryKey(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() + } + TableConstraint::FulltextOrSpatial(constraint) => { + constraint.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 39ca84c9f..54a158c1f 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -170,8 +170,10 @@ pub enum Token { RBrace, /// Right Arrow `=>` RArrow, - /// Sharp `#` used for PostgreSQL Bitwise XOR operator + /// Sharp `#` used for PostgreSQL Bitwise XOR operator, also PostgreSQL/Redshift geometrical unary/binary operator (Number of points in path or polygon/Intersection) Sharp, + /// `##` PostgreSQL/Redshift geometrical binary operator (Point of closest proximity) + DoubleSharp, /// Tilde `~` used for PostgreSQL Bitwise NOT operator or case sensitive match regular expression operator Tilde, /// `~*` , a case insensitive match regular expression operator in PostgreSQL @@ -198,7 +200,7 @@ pub enum Token { ExclamationMark, /// Double Exclamation Mark `!!` used for PostgreSQL prefix factorial operator DoubleExclamationMark, - /// AtSign `@` used for PostgreSQL abs operator + /// AtSign `@` used for PostgreSQL abs operator, also PostgreSQL/Redshift geometrical unary/binary operator (Center, Contained or on) AtSign, /// `^@`, a "starts with" string operator in PostgreSQL CaretAt, @@ -214,6 +216,38 @@ pub enum Token { LongArrow, /// `#>`, extracts JSON sub-object at the specified path HashArrow, + /// `@-@` PostgreSQL/Redshift geometrical unary operator (Length or circumference) + AtDashAt, + /// `?-` PostgreSQL/Redshift geometrical unary/binary operator (Is horizontal?/Are horizontally aligned?) + QuestionMarkDash, + /// `&<` PostgreSQL/Redshift geometrical binary operator (Overlaps to left?) + AmpersandLeftAngleBracket, + /// `&>` PostgreSQL/Redshift geometrical binary operator (Overlaps to right?)` + AmpersandRightAngleBracket, + /// `&<|` PostgreSQL/Redshift geometrical binary operator (Does not extend above?)` + AmpersandLeftAngleBracketVerticalBar, + /// `|&>` PostgreSQL/Redshift geometrical binary operator (Does not extend below?)` + VerticalBarAmpersandRightAngleBracket, + /// `<->` PostgreSQL/Redshift geometrical binary operator (Distance between) + TwoWayArrow, + /// `<^` PostgreSQL/Redshift geometrical binary operator (Is below?) + LeftAngleBracketCaret, + /// `>^` PostgreSQL/Redshift geometrical binary operator (Is above?) + RightAngleBracketCaret, + /// `?#` PostgreSQL/Redshift geometrical binary operator (Intersects or overlaps) + QuestionMarkSharp, + /// `?-|` PostgreSQL/Redshift geometrical binary operator (Is perpendicular?) + QuestionMarkDashVerticalBar, + /// `?||` PostgreSQL/Redshift geometrical binary operator (Are parallel?) + QuestionMarkDoubleVerticalBar, + /// `~=` PostgreSQL/Redshift geometrical binary operator (Same as) + TildeEqual, + /// `<<| PostgreSQL/Redshift geometrical binary operator (Is strictly below?) + 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 @@ -303,6 +337,7 @@ impl fmt::Display for Token { Token::RBrace => f.write_str("}"), Token::RArrow => f.write_str("=>"), Token::Sharp => f.write_str("#"), + Token::DoubleSharp => f.write_str("##"), Token::ExclamationMark => f.write_str("!"), Token::DoubleExclamationMark => f.write_str("!!"), Token::Tilde => f.write_str("~"), @@ -320,6 +355,22 @@ impl fmt::Display for Token { Token::Overlap => f.write_str("&&"), Token::PGSquareRoot => f.write_str("|/"), Token::PGCubeRoot => f.write_str("||/"), + Token::AtDashAt => f.write_str("@-@"), + Token::QuestionMarkDash => f.write_str("?-"), + Token::AmpersandLeftAngleBracket => f.write_str("&<"), + 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(">^"), + Token::QuestionMarkSharp => f.write_str("?#"), + Token::QuestionMarkDashVerticalBar => f.write_str("?-|"), + Token::QuestionMarkDoubleVerticalBar => f.write_str("?||"), + Token::TildeEqual => f.write_str("~="), + Token::ShiftLeftVerticalBar => f.write_str("<<|"), + Token::VerticalBarShiftRight => f.write_str("|>>"), Token::Placeholder(ref s) => write!(f, "{s}"), Token::Arrow => write!(f, "->"), Token::LongArrow => write!(f, "->>"), @@ -847,7 +898,7 @@ impl<'a> Tokenizer<'a> { }; let mut location = state.location(); - while let Some(token) = self.next_token(&mut state)? { + while let Some(token) = self.next_token(&mut state, buf.last().map(|t| &t.token))? { let span = location.span_to(state.location()); buf.push(TokenWithSpan { token, span }); @@ -884,7 +935,11 @@ impl<'a> Tokenizer<'a> { } /// Get the next token or return None - fn next_token(&self, chars: &mut State) -> Result, TokenizerError> { + fn next_token( + &self, + chars: &mut State, + prev_token: Option<&Token>, + ) -> Result, TokenizerError> { match chars.peek() { Some(&ch) => match ch { ' ' => self.consume_and_return(chars, Token::Whitespace(Whitespace::Space)), @@ -971,7 +1026,10 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('\'') => { // N'...' - a - let s = self.tokenize_single_quoted_string(chars, '\'', true)?; + let backslash_escape = + self.dialect.supports_string_literal_backslash_escape(); + let s = + self.tokenize_single_quoted_string(chars, '\'', backslash_escape)?; Ok(Some(Token::NationalStringLiteral(s))) } _ => { @@ -982,7 +1040,7 @@ impl<'a> Tokenizer<'a> { } } // PostgreSQL accepts "escape" string constants, which are an extension to the SQL standard. - x @ 'e' | x @ 'E' => { + x @ 'e' | x @ 'E' if self.dialect.supports_string_escape_constant() => { let starting_loc = chars.location(); chars.next(); // consume, to check the next char match chars.peek() { @@ -1133,12 +1191,40 @@ impl<'a> Tokenizer<'a> { } // numbers and period '0'..='9' | '.' => { - let mut s = peeking_take_while(chars, |ch| ch.is_ascii_digit()); + // 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| { + self.dialect.supports_numeric_literal_underscores() + && ch == '_' + && next_char.is_some_and(|next_ch| next_ch.is_ascii_hexdigit()) + }; + + let mut s = peeking_next_take_while(chars, |ch, next_ch| { + ch.is_ascii_digit() || is_number_separator(ch, next_ch) + }); // match binary literal that starts with 0x if s == "0" && chars.peek() == Some(&'x') { chars.next(); - let s2 = peeking_take_while(chars, |ch| ch.is_ascii_hexdigit()); + let s2 = peeking_next_take_while(chars, |ch, next_ch| { + ch.is_ascii_hexdigit() || is_number_separator(ch, next_ch) + }); return Ok(Some(Token::HexStringLiteral(s2))); } @@ -1147,15 +1233,30 @@ impl<'a> Tokenizer<'a> { s.push('.'); chars.next(); } - s += &peeking_take_while(chars, |ch| ch.is_ascii_digit()); - // No number -> Token::Period + // If the dialect supports identifiers that start with a numeric prefix + // and we have now consumed a dot, check if the previous token was a Word. + // If so, what follows is definitely not part of a decimal number and + // we should yield the dot as a dedicated token so compound identifiers + // starting with digits can be parsed correctly. + if s == "." && self.dialect.supports_numeric_prefix() { + if let Some(Token::Word(_)) = prev_token { + return Ok(Some(Token::Period)); + } + } + + // Consume fractional digits. + s += &peeking_next_take_while(chars, |ch, next_ch| { + ch.is_ascii_digit() || is_number_separator(ch, next_ch) + }); + + // No fraction -> Token::Period if s == "." { return Ok(Some(Token::Period)); } - let mut exponent_part = String::new(); // Parse exponent as number + let mut exponent_part = String::new(); if chars.peek() == Some(&'e') || chars.peek() == Some(&'E') { let mut char_clone = chars.peekable.clone(); exponent_part.push(char_clone.next().unwrap()); @@ -1184,14 +1285,23 @@ impl<'a> Tokenizer<'a> { } } - // mysql dialect supports identifiers that start with a numeric prefix, - // as long as they aren't an exponent number. - if self.dialect.supports_numeric_prefix() && exponent_part.is_empty() { - let word = - peeking_take_while(chars, |ch| self.dialect.is_identifier_part(ch)); - - if !word.is_empty() { - s += word.as_str(); + // If the dialect supports identifiers that start with a numeric prefix, + // we need to check if the value is in fact an identifier and must thus + // be tokenized as a word. + if self.dialect.supports_numeric_prefix() { + if exponent_part.is_empty() { + // If it is not a number with an exponent, it may be + // an identifier starting with digits. + let word = + peeking_take_while(chars, |ch| self.dialect.is_identifier_part(ch)); + + if !word.is_empty() { + s += word.as_str(); + return Ok(Some(Token::make_word(s.as_str(), None))); + } + } else if prev_token == Some(&Token::Period) { + // If the previous token was a period, thus not belonging to a number, + // the value we have is part of an identifier. return Ok(Some(Token::make_word(s.as_str(), None))); } } @@ -1211,14 +1321,26 @@ impl<'a> Tokenizer<'a> { // operators '-' => { chars.next(); // consume the '-' + match chars.peek() { Some('-') => { - chars.next(); // consume the second '-', starting a single-line comment - let comment = self.tokenize_single_line_comment(chars); - Ok(Some(Token::Whitespace(Whitespace::SingleLineComment { - prefix: "--".to_owned(), - comment, - }))) + let mut is_comment = true; + if self.dialect.requires_single_line_comment_whitespace() { + is_comment = Some(' ') == chars.peekable.clone().nth(1); + } + + if is_comment { + chars.next(); // consume second '-' + let comment = self.tokenize_single_line_comment(chars); + return Ok(Some(Token::Whitespace( + Whitespace::SingleLineComment { + prefix: "--".to_owned(), + comment, + }, + ))); + } + + self.start_binop(chars, "-", Token::Minus) } Some('>') => { chars.next(); @@ -1278,6 +1400,31 @@ impl<'a> Tokenizer<'a> { _ => self.start_binop(chars, "||", Token::StringConcat), } } + Some('&') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('>') => self.consume_for_binop( + chars, + "|&>", + Token::VerticalBarAmpersandRightAngleBracket, + ), + _ => self.start_binop_opt(chars, "|&", None), + } + } + Some('>') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('>') => self.consume_for_binop( + chars, + "|>>", + Token::VerticalBarShiftRight, + ), + _ => 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), } @@ -1326,8 +1473,34 @@ impl<'a> Tokenizer<'a> { _ => self.start_binop(chars, "<=", Token::LtEq), } } + Some('|') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, "<<|", Token::ShiftLeftVerticalBar) + } Some('>') => self.consume_for_binop(chars, "<>", Token::Neq), + Some('<') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('|') => self.consume_for_binop( + chars, + "<<|", + Token::ShiftLeftVerticalBar, + ), + _ => self.start_binop(chars, "<<", Token::ShiftLeft), + } + } Some('<') => self.consume_for_binop(chars, "<<", Token::ShiftLeft), + Some('-') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('>') => { + self.consume_for_binop(chars, "<->", Token::TwoWayArrow) + } + _ => self.start_binop_opt(chars, "<-", None), + } + } + Some('^') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, "<^", Token::LeftAngleBracketCaret) + } Some('@') => self.consume_for_binop(chars, "<@", Token::ArrowAt), _ => self.start_binop(chars, "<", Token::Lt), } @@ -1337,6 +1510,9 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('=') => self.consume_for_binop(chars, ">=", Token::GtEq), Some('>') => self.consume_for_binop(chars, ">>", Token::ShiftRight), + Some('^') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, ">^", Token::RightAngleBracketCaret) + } _ => self.start_binop(chars, ">", Token::Gt), } } @@ -1355,6 +1531,22 @@ impl<'a> Tokenizer<'a> { '&' => { chars.next(); // consume the '&' match chars.peek() { + Some('>') if self.dialect.supports_geometric_types() => { + chars.next(); + self.consume_and_return(chars, Token::AmpersandRightAngleBracket) + } + Some('<') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('|') => self.consume_and_return( + chars, + Token::AmpersandLeftAngleBracketVerticalBar, + ), + _ => { + self.start_binop(chars, "&<", Token::AmpersandLeftAngleBracket) + } + } + } Some('&') => { chars.next(); // consume the second '&' self.start_binop(chars, "&&", Token::Overlap) @@ -1385,6 +1577,9 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume match chars.peek() { Some('*') => self.consume_for_binop(chars, "~*", Token::TildeAsterisk), + Some('=') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, "~=", Token::TildeEqual) + } Some('~') => { chars.next(); match chars.peek() { @@ -1411,6 +1606,9 @@ impl<'a> Tokenizer<'a> { } } Some(' ') => Ok(Some(Token::Sharp)), + Some('#') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, "##", Token::DoubleSharp) + } Some(sch) if self.dialect.is_identifier_start('#') => { self.tokenize_identifier_or_keyword([ch, *sch], chars) } @@ -1420,6 +1618,16 @@ impl<'a> Tokenizer<'a> { '@' => { chars.next(); match chars.peek() { + Some('@') if self.dialect.supports_geometric_types() => { + self.consume_and_return(chars, Token::AtAt) + } + Some('-') if self.dialect.supports_geometric_types() => { + chars.next(); + match chars.peek() { + Some('@') => self.consume_and_return(chars, Token::AtDashAt), + _ => self.start_binop_opt(chars, "@-", None), + } + } Some('>') => self.consume_and_return(chars, Token::AtArrow), Some('?') => self.consume_and_return(chars, Token::AtQuestion), Some('@') => { @@ -1452,11 +1660,30 @@ impl<'a> Tokenizer<'a> { } } // Postgres uses ? for jsonb operators, not prepared statements - '?' if dialect_of!(self is PostgreSqlDialect) => { - chars.next(); + '?' if self.dialect.supports_geometric_types() => { + chars.next(); // consume match chars.peek() { - Some('|') => self.consume_and_return(chars, Token::QuestionPipe), + Some('|') => { + chars.next(); + match chars.peek() { + Some('|') => self.consume_and_return( + chars, + Token::QuestionMarkDoubleVerticalBar, + ), + _ => Ok(Some(Token::QuestionPipe)), + } + } + Some('&') => self.consume_and_return(chars, Token::QuestionAnd), + Some('-') => { + chars.next(); // consume + match chars.peek() { + Some('|') => self + .consume_and_return(chars, Token::QuestionMarkDashVerticalBar), + _ => Ok(Some(Token::QuestionMarkDash)), + } + } + Some('#') => self.consume_and_return(chars, Token::QuestionMarkSharp), _ => self.consume_and_return(chars, Token::Question), } } @@ -1490,7 +1717,7 @@ impl<'a> Tokenizer<'a> { default: Token, ) -> Result, TokenizerError> { chars.next(); // consume the first char - self.start_binop(chars, prefix, default) + self.start_binop_opt(chars, prefix, Some(default)) } /// parse a custom binary operator @@ -1499,6 +1726,16 @@ impl<'a> Tokenizer<'a> { chars: &mut State, prefix: &str, default: Token, + ) -> Result, TokenizerError> { + self.start_binop_opt(chars, prefix, Some(default)) + } + + /// parse a custom binary operator + fn start_binop_opt( + &self, + chars: &mut State, + prefix: &str, + default: Option, ) -> Result, TokenizerError> { let mut custom = None; while let Some(&ch) = chars.peek() { @@ -1509,10 +1746,14 @@ impl<'a> Tokenizer<'a> { custom.get_or_insert_with(|| prefix.to_string()).push(ch); chars.next(); } - - Ok(Some( - custom.map(Token::CustomBinaryOperator).unwrap_or(default), - )) + match (custom, default) { + (Some(custom), _) => Ok(Token::CustomBinaryOperator(custom).into()), + (None, Some(tok)) => Ok(Some(tok)), + (None, None) => self.tokenizer_error( + chars.location(), + format!("Expected a valid binary operator after '{prefix}'"), + ), + } } /// Tokenize dollar preceded value (i.e: a string/placeholder) @@ -1568,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() { @@ -1817,8 +2058,13 @@ impl<'a> Tokenizer<'a> { num_consecutive_quotes = 0; if let Some(next) = chars.peek() { - if !self.unescape { - // In no-escape mode, the given query has to be saved completely including backslashes. + if !self.unescape + || (self.dialect.ignores_wildcard_escapes() + && (*next == '%' || *next == '_')) + { + // In no-escape mode, the given query has to be saved completely + // including backslashes. Similarly, with ignore_like_wildcard_escapes, + // the backslash is not stripped. s.push(ch); s.push(*next); chars.next(); // consume next @@ -1943,6 +2189,24 @@ fn peeking_take_while(chars: &mut State, mut predicate: impl FnMut(char) -> bool s } +/// Same as peeking_take_while, but also passes the next character to the predicate. +fn peeking_next_take_while( + chars: &mut State, + mut predicate: impl FnMut(char, Option) -> bool, +) -> String { + let mut s = String::new(); + while let Some(&ch) = chars.peek() { + let next_char = chars.peekable.clone().nth(1); + if predicate(ch, next_char) { + chars.next(); // consume + s.push(ch); + } else { + break; + } + } + s +} + fn unescape_single_quoted_string(chars: &mut State<'_>) -> Option { Unescape::new(chars).unescape() } @@ -2138,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(), }) } @@ -2155,6 +2419,7 @@ mod tests { use crate::dialect::{ BigQueryDialect, ClickHouseDialect, HiveDialect, MsSqlDialect, MySqlDialect, SQLiteDialect, }; + use crate::test_utils::{all_dialects_except, all_dialects_where}; use core::fmt::Debug; #[test] @@ -2223,6 +2488,41 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_numeric_literal_underscore() { + let dialect = GenericDialect {}; + let sql = String::from("SELECT 10_000"); + let mut tokenizer = Tokenizer::new(&dialect, &sql); + let tokens = tokenizer.tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Number("10".to_string(), false), + Token::make_word("_000", None), + ]; + compare(expected, tokens); + + all_dialects_where(|dialect| dialect.supports_numeric_literal_underscores()).tokenizes_to( + "SELECT 10_000, _10_000, 10_00_, 10___0", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Number("10_000".to_string(), false), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::make_word("_10_000", None), // leading underscore tokenizes as a word (parsed as column identifier) + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Number("10_00".to_string(), false), + Token::make_word("_", None), // trailing underscores tokenizes as a word (syntax error in some dialects) + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Number("10".to_string(), false), + Token::make_word("___0", None), // multiple underscores tokenizes as a word (syntax error in some dialects) + ], + ); + } + #[test] fn tokenize_select_exponent() { let sql = String::from("SELECT 1e10, 1e-10, 1e+10, 1ea, 1e-10a, 1e-10-10"); @@ -2869,90 +3169,79 @@ mod tests { #[test] fn tokenize_nested_multiline_comment() { - let dialect = GenericDialect {}; - let test_cases = vec![ - ( - "0/*multi-line\n* \n/* comment \n /*comment*/*/ */ /comment*/1", - vec![ - Token::Number("0".to_string(), false), - Token::Whitespace(Whitespace::MultiLineComment( - "multi-line\n* \n/* comment \n /*comment*/*/ ".into(), - )), - Token::Whitespace(Whitespace::Space), - Token::Div, - Token::Word(Word { - value: "comment".to_string(), - quote_style: None, - keyword: Keyword::COMMENT, - }), - Token::Mul, - Token::Div, - Token::Number("1".to_string(), false), - ], - ), - ( - "0/*multi-line\n* \n/* comment \n /*comment/**/ */ /comment*/*/1", - vec![ - Token::Number("0".to_string(), false), - Token::Whitespace(Whitespace::MultiLineComment( - "multi-line\n* \n/* comment \n /*comment/**/ */ /comment*/".into(), - )), - Token::Number("1".to_string(), false), - ], - ), - ( - "SELECT 1/* a /* b */ c */0", - vec![ - Token::make_keyword("SELECT"), - Token::Whitespace(Whitespace::Space), - Token::Number("1".to_string(), false), - Token::Whitespace(Whitespace::MultiLineComment(" a /* b */ c ".to_string())), - Token::Number("0".to_string(), false), - ], - ), - ]; + all_dialects_where(|d| d.supports_nested_comments()).tokenizes_to( + "0/*multi-line\n* \n/* comment \n /*comment*/*/ */ /comment*/1", + vec![ + Token::Number("0".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment( + "multi-line\n* \n/* comment \n /*comment*/*/ ".into(), + )), + Token::Whitespace(Whitespace::Space), + Token::Div, + Token::Word(Word { + value: "comment".to_string(), + quote_style: None, + keyword: Keyword::COMMENT, + }), + Token::Mul, + Token::Div, + Token::Number("1".to_string(), false), + ], + ); - for (sql, expected) in test_cases { - let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); - compare(expected, tokens); - } + all_dialects_where(|d| d.supports_nested_comments()).tokenizes_to( + "0/*multi-line\n* \n/* comment \n /*comment/**/ */ /comment*/*/1", + vec![ + Token::Number("0".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment( + "multi-line\n* \n/* comment \n /*comment/**/ */ /comment*/".into(), + )), + Token::Number("1".to_string(), false), + ], + ); + + all_dialects_where(|d| d.supports_nested_comments()).tokenizes_to( + "SELECT 1/* a /* b */ c */0", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Number("1".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment(" a /* b */ c ".to_string())), + Token::Number("0".to_string(), false), + ], + ); } #[test] fn tokenize_nested_multiline_comment_empty() { - let sql = "select 1/*/**/*/0"; - - let dialect = GenericDialect {}; - let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); - let expected = vec![ - Token::make_keyword("select"), - Token::Whitespace(Whitespace::Space), - Token::Number("1".to_string(), false), - Token::Whitespace(Whitespace::MultiLineComment("/**/".to_string())), - Token::Number("0".to_string(), false), - ]; - - compare(expected, tokens); + all_dialects_where(|d| d.supports_nested_comments()).tokenizes_to( + "select 1/*/**/*/0", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::Number("1".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment("/**/".to_string())), + Token::Number("0".to_string(), false), + ], + ); } #[test] fn tokenize_nested_comments_if_not_supported() { - let dialect = SQLiteDialect {}; - let sql = "SELECT 1/*/* nested comment */*/0"; - let tokens = Tokenizer::new(&dialect, sql).tokenize(); - let expected = vec![ - Token::make_keyword("SELECT"), - Token::Whitespace(Whitespace::Space), - Token::Number("1".to_string(), false), - Token::Whitespace(Whitespace::MultiLineComment( - "/* nested comment ".to_string(), - )), - Token::Mul, - Token::Div, - Token::Number("0".to_string(), false), - ]; - - compare(expected, tokens.unwrap()); + all_dialects_except(|d| d.supports_nested_comments()).tokenizes_to( + "SELECT 1/*/* nested comment */*/0", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Number("1".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment( + "/* nested comment ".to_string(), + )), + Token::Mul, + Token::Div, + Token::Number("0".to_string(), false), + ], + ); } #[test] @@ -3204,7 +3493,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, @@ -3337,6 +3626,9 @@ mod tests { (r#"'\\a\\b\'c'"#, r#"\\a\\b\'c"#, r#"\a\b'c"#), (r#"'\'abcd'"#, r#"\'abcd"#, r#"'abcd"#), (r#"'''a''b'"#, r#"''a''b"#, r#"'a'b"#), + (r#"'\q'"#, r#"\q"#, r#"q"#), + (r#"'\%\_'"#, r#"\%\_"#, r#"%_"#), + (r#"'\\%\\_'"#, r#"\\%\\_"#, r#"\%\_"#), ] { let tokens = Tokenizer::new(&dialect, sql) .with_unescape(false) @@ -3370,6 +3662,16 @@ mod tests { compare(expected, tokens); } + + // MySQL special case for LIKE escapes + for (sql, expected) in [(r#"'\%'"#, r#"\%"#), (r#"'\_'"#, r#"\_"#)] { + let dialect = MySqlDialect {}; + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); + + let expected = vec![Token::SingleQuotedString(expected.to_string())]; + + compare(expected, tokens); + } } #[test] @@ -3543,4 +3845,218 @@ mod tests { ]; compare(expected, tokens); } + + #[test] + fn test_national_strings_backslash_escape_not_supported() { + all_dialects_where(|dialect| !dialect.supports_string_literal_backslash_escape()) + .tokenizes_to( + "select n'''''\\'", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::NationalStringLiteral("''\\".to_string()), + ], + ); + } + + #[test] + fn test_national_strings_backslash_escape_supported() { + all_dialects_where(|dialect| dialect.supports_string_literal_backslash_escape()) + .tokenizes_to( + "select n'''''\\''", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::NationalStringLiteral("'''".to_string()), + ], + ); + } + + #[test] + fn test_string_escape_constant_not_supported() { + all_dialects_where(|dialect| !dialect.supports_string_escape_constant()).tokenizes_to( + "select e'...'", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::make_word("e", None), + Token::SingleQuotedString("...".to_string()), + ], + ); + + all_dialects_where(|dialect| !dialect.supports_string_escape_constant()).tokenizes_to( + "select E'...'", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::make_word("E", None), + Token::SingleQuotedString("...".to_string()), + ], + ); + } + + #[test] + fn test_string_escape_constant_supported() { + all_dialects_where(|dialect| dialect.supports_string_escape_constant()).tokenizes_to( + "select e'\\''", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::EscapedStringLiteral("'".to_string()), + ], + ); + + all_dialects_where(|dialect| dialect.supports_string_escape_constant()).tokenizes_to( + "select E'\\''", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::EscapedStringLiteral("'".to_string()), + ], + ); + } + + #[test] + fn test_whitespace_required_after_single_line_comment() { + all_dialects_where(|dialect| dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT --'abc'", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Minus, + Token::Minus, + Token::SingleQuotedString("abc".to_string()), + ], + ); + + all_dialects_where(|dialect| dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT -- 'abc'", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: " 'abc'".to_string(), + }), + ], + ); + + all_dialects_where(|dialect| dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT --", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Minus, + Token::Minus, + ], + ); + } + + #[test] + fn test_whitespace_not_required_after_single_line_comment() { + all_dialects_where(|dialect| !dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT --'abc'", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: "'abc'".to_string(), + }), + ], + ); + + all_dialects_where(|dialect| !dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT -- 'abc'", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: " 'abc'".to_string(), + }), + ], + ); + + all_dialects_where(|dialect| !dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT --", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: "".to_string(), + }), + ], + ); + } + + #[test] + fn test_tokenize_identifiers_numeric_prefix() { + all_dialects_where(|dialect| dialect.supports_numeric_prefix()) + .tokenizes_to("123abc", vec![Token::make_word("123abc", None)]); + + all_dialects_where(|dialect| dialect.supports_numeric_prefix()) + .tokenizes_to("12e34", vec![Token::Number("12e34".to_string(), false)]); + + all_dialects_where(|dialect| dialect.supports_numeric_prefix()).tokenizes_to( + "t.12e34", + vec![ + Token::make_word("t", None), + Token::Period, + Token::make_word("12e34", None), + ], + ); + + all_dialects_where(|dialect| dialect.supports_numeric_prefix()).tokenizes_to( + "t.1two3", + vec![ + Token::make_word("t", None), + Token::Period, + Token::make_word("1two3", None), + ], + ); + } + + #[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 9dfabc014..0ef1c4f0c 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -20,128 +20,156 @@ 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] fn parse_literal_string() { let sql = concat!( - "SELECT ", - "'single', ", - r#""double", "#, - "'''triple-single''', ", - r#""""triple-double""", "#, - r#"'single\'escaped', "#, - r#"'''triple-single\'escaped''', "#, - r#"'''triple-single'unescaped''', "#, - r#""double\"escaped", "#, - r#""""triple-double\"escaped""", "#, - r#""""triple-double"unescaped""""#, + "SELECT ", // line 1, column 1 + "'single', ", // line 1, column 7 + r#""double", "#, // line 1, column 14 + "'''triple-single''', ", // line 1, column 22 + r#""""triple-double""", "#, // line 1, column 33 + r#"'single\'escaped', "#, // line 1, column 43 + r#"'''triple-single\'escaped''', "#, // line 1, column 55 + r#"'''triple-single'unescaped''', "#, // line 1, column 68 + r#""double\"escaped", "#, // line 1, column 83 + r#""""triple-double\"escaped""", "#, // line 1, column 92 + r#""""triple-double"unescaped""", "#, // line 1, column 105 + r#""""triple-double'unescaped""", "#, // line 1, column 118 + r#"'''triple-single"unescaped'''"#, // line 1, column 131 ); let dialect = TestedDialects::new_with_options( vec![Box::new(BigQueryDialect {})], ParserOptions::new().with_unescape(false), ); let select = dialect.verified_only_select(sql); - assert_eq!(10, select.projection.len()); + assert_eq!(12, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedString("single".to_string())), + &Expr::Value(Value::SingleQuotedString("single".into()).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedString("double".to_string())), + &Expr::Value(Value::DoubleQuotedString("double".into()).with_empty_span()), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedString("triple-single".to_string())), + &Expr::Value(Value::TripleSingleQuotedString("triple-single".into()).with_empty_span()), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedString("triple-double".to_string())), + &Expr::Value(Value::TripleDoubleQuotedString("triple-double".into()).with_empty_span()), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::SingleQuotedString(r#"single\'escaped"#.to_string())), + &Expr::Value(Value::SingleQuotedString(r#"single\'escaped"#.into()).with_empty_span()), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedString( - r#"triple-single\'escaped"#.to_string() - )), + &Expr::Value( + Value::TripleSingleQuotedString(r#"triple-single\'escaped"#.into()).with_empty_span() + ), expr_from_projection(&select.projection[5]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedString( - r#"triple-single'unescaped"#.to_string() - )), + &Expr::Value( + Value::TripleSingleQuotedString(r#"triple-single'unescaped"#.into()).with_empty_span() + ), expr_from_projection(&select.projection[6]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedString(r#"double\"escaped"#.to_string())), + &Expr::Value(Value::DoubleQuotedString(r#"double\"escaped"#.to_string()).with_empty_span()), expr_from_projection(&select.projection[7]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedString( - r#"triple-double\"escaped"#.to_string() - )), + &Expr::Value( + Value::TripleDoubleQuotedString(r#"triple-double\"escaped"#.to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[8]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedString( - r#"triple-double"unescaped"#.to_string() - )), + &Expr::Value( + Value::TripleDoubleQuotedString(r#"triple-double"unescaped"#.to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[9]) ); + assert_eq!( + &Expr::Value( + Value::TripleDoubleQuotedString(r#"triple-double'unescaped"#.to_string()) + .with_empty_span() + ), + expr_from_projection(&select.projection[10]) + ); + assert_eq!( + &Expr::Value( + Value::TripleSingleQuotedString(r#"triple-single"unescaped"#.to_string()) + .with_empty_span() + ), + expr_from_projection(&select.projection[11]) + ); } #[test] fn parse_byte_literal() { let sql = concat!( - "SELECT ", - "B'abc', ", - r#"B"abc", "#, - r#"B'f\(abc,(.*),def\)', "#, - r#"B"f\(abc,(.*),def\)", "#, - r#"B'''abc''', "#, - r#"B"""abc""""#, + "SELECT ", // line 1, column 1 + "B'abc', ", // line 1, column 8 + r#"B"abc", "#, // line 1, column 15 + r#"B'f\(abc,(.*),def\)', "#, // line 1, column 22 + r#"B"f\(abc,(.*),def\)", "#, // line 1, column 42 + r#"B'''abc''', "#, // line 1, column 62 + r#"B"""abc""""#, // line 1, column 74 ); let stmt = bigquery().verified_stmt(sql); if let Statement::Query(query) = stmt { if let SetExpr::Select(select) = *query.body { assert_eq!(6, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedByteStringLiteral("abc".to_string())), + &Expr::Value( + Value::SingleQuotedByteStringLiteral("abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedByteStringLiteral("abc".to_string())), + &Expr::Value( + Value::DoubleQuotedByteStringLiteral("abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::SingleQuotedByteStringLiteral( - r"f\(abc,(.*),def\)".to_string() - )), + &Expr::Value( + Value::SingleQuotedByteStringLiteral(r"f\(abc,(.*),def\)".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedByteStringLiteral( - r"f\(abc,(.*),def\)".to_string() - )), + &Expr::Value( + Value::DoubleQuotedByteStringLiteral(r"f\(abc,(.*),def\)".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedByteStringLiteral( - r"abc".to_string() - )), + &Expr::Value( + Value::TripleSingleQuotedByteStringLiteral(r"abc".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedByteStringLiteral( - r"abc".to_string() - )), + &Expr::Value( + Value::TripleDoubleQuotedByteStringLiteral(r"abc".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[5]) ); } @@ -158,48 +186,54 @@ fn parse_byte_literal() { #[test] fn parse_raw_literal() { let sql = concat!( - "SELECT ", - "R'abc', ", - r#"R"abc", "#, - r#"R'f\(abc,(.*),def\)', "#, - r#"R"f\(abc,(.*),def\)", "#, - r#"R'''abc''', "#, - r#"R"""abc""""#, + "SELECT ", // line 1, column 1 + "R'abc', ", // line 1, column 8 + r#"R"abc", "#, // line 1, column 15 + r#"R'f\(abc,(.*),def\)', "#, // line 1, column 22 + r#"R"f\(abc,(.*),def\)", "#, // line 1, column 42 + r#"R'''abc''', "#, // line 1, column 62 + r#"R"""abc""""#, // line 1, column 74 ); let stmt = bigquery().verified_stmt(sql); if let Statement::Query(query) = stmt { if let SetExpr::Select(select) = *query.body { assert_eq!(6, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedRawStringLiteral("abc".to_string())), + &Expr::Value( + Value::SingleQuotedRawStringLiteral("abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedRawStringLiteral("abc".to_string())), + &Expr::Value( + Value::DoubleQuotedRawStringLiteral("abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::SingleQuotedRawStringLiteral( - r"f\(abc,(.*),def\)".to_string() - )), + &Expr::Value( + Value::SingleQuotedRawStringLiteral(r"f\(abc,(.*),def\)".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedRawStringLiteral( - r"f\(abc,(.*),def\)".to_string() - )), + &Expr::Value( + Value::DoubleQuotedRawStringLiteral(r"f\(abc,(.*),def\)".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedRawStringLiteral( - r"abc".to_string() - )), + &Expr::Value( + Value::TripleSingleQuotedRawStringLiteral(r"abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedRawStringLiteral( - r"abc".to_string() - )), + &Expr::Value( + Value::TripleDoubleQuotedRawStringLiteral(r"abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[5]) ); } @@ -213,6 +247,64 @@ fn parse_raw_literal() { ); } +#[test] +fn parse_big_query_non_reserved_column_alias() { + let sql = r#"SELECT OFFSET, EXPLAIN, ANALYZE, SORT, TOP, VIEW FROM T"#; + bigquery().verified_stmt(sql); + + let sql = r#"SELECT 1 AS OFFSET, 2 AS EXPLAIN, 3 AS ANALYZE FROM T"#; + bigquery().verified_stmt(sql); +} + +#[test] +fn parse_at_at_identifier() { + bigquery().verified_stmt("SELECT @@error.stack_trace, @@error.message"); +} + +#[test] +fn parse_begin() { + 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, + has_end_keyword, + .. + } = bigquery().verified_stmt(sql) + else { + unreachable!(); + }; + assert_eq!(1, statements.len()); + assert!(exception.is_some()); + + let exception = exception.unwrap(); + assert_eq!(1, exception.len()); + assert!(has_end_keyword); + + bigquery().verified_stmt( + "BEGIN SELECT 1; SELECT 2; EXCEPTION WHEN ERROR THEN SELECT 2; SELECT 4; END", + ); + bigquery() + .verified_stmt("BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT @@error.stack_trace; END"); + bigquery().verified_stmt("BEGIN EXCEPTION WHEN ERROR THEN SELECT 2; END"); + bigquery().verified_stmt("BEGIN SELECT 1; SELECT 2; EXCEPTION WHEN ERROR THEN END"); + bigquery().verified_stmt("BEGIN EXCEPTION WHEN ERROR THEN END"); + bigquery().verified_stmt("BEGIN SELECT 1; SELECT 2; END"); + bigquery().verified_stmt("BEGIN END"); + + assert_eq!( + bigquery() + .parse_sql_statements("BEGIN SELECT 1; SELECT 2 END") + .unwrap_err(), + ParserError::ParserError("Expected: ;, found: END".to_string()) + ); + assert_eq!( + bigquery() + .parse_sql_statements("BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2 END") + .unwrap_err(), + ParserError::ParserError("Expected: ;, found: END".to_string()) + ); +} + #[test] fn parse_delete_statement() { let sql = "DELETE \"table\" WHERE 1"; @@ -222,7 +314,7 @@ fn parse_delete_statement() { .. }) => { assert_eq!( - table_from_name(ObjectName(vec![Ident::with_quote('"', "table")])), + table_from_name(ObjectName::from(vec![Ident::with_quote('"', "table")])), from[0].relation ); } @@ -240,16 +332,16 @@ fn parse_create_view_with_options() { "AS SELECT column_1, column_2, column_3 FROM myproject.mydataset.mytable", ); match bigquery().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, query, options, columns, .. - } => { + }) => { assert_eq!( name, - ObjectName(vec![ + ObjectName::from(vec![ "myproject".into(), "mydataset".into(), "newview".into() @@ -265,10 +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())), - }])]), + 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 @@ -287,9 +385,10 @@ fn parse_create_view_with_options() { assert_eq!( &SqlOption::KeyValue { key: Ident::new("description"), - value: Expr::Value(Value::DoubleQuotedString( - "a view that expires in 2 days".to_string() - )), + value: Expr::Value( + Value::DoubleQuotedString("a view that expires in 2 days".to_string()) + .with_empty_span() + ), }, &options[2], ); @@ -297,11 +396,12 @@ fn parse_create_view_with_options() { _ => unreachable!(), } } + #[test] fn parse_create_view_if_not_exists() { let sql = "CREATE VIEW IF NOT EXISTS mydataset.newview AS SELECT foo FROM bar"; match bigquery().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, columns, query, @@ -314,7 +414,7 @@ fn parse_create_view_if_not_exists() { if_not_exists, temporary, .. - } => { + }) => { assert_eq!("mydataset.newview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -335,12 +435,12 @@ fn parse_create_view_if_not_exists() { fn parse_create_view_with_unquoted_hyphen() { let sql = "CREATE VIEW IF NOT EXISTS my-pro-ject.mydataset.myview AS SELECT 1"; match bigquery().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, query, if_not_exists, .. - } => { + }) => { assert_eq!("my-pro-ject.mydataset.myview", name.to_string()); assert_eq!("SELECT 1", query.to_string()); assert!(if_not_exists); @@ -356,7 +456,7 @@ fn parse_create_table_with_unquoted_hyphen() { Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!( name, - ObjectName(vec![ + ObjectName::from(vec![ "my-pro-ject".into(), "mydataset".into(), "mytable".into() @@ -366,7 +466,6 @@ fn parse_create_table_with_unquoted_hyphen() { vec![ColumnDef { name: Ident::new("x"), data_type: DataType::Int64, - collation: None, options: vec![] },], columns @@ -392,19 +491,18 @@ fn parse_create_table_with_options() { columns, partition_by, cluster_by, - options, + table_options, .. }) => { assert_eq!( name, - ObjectName(vec!["mydataset".into(), "newtable".into()]) + ObjectName::from(vec!["mydataset".into(), "newtable".into()]) ); assert_eq!( vec![ ColumnDef { name: Ident::new("x"), data_type: DataType::Int64, - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -414,9 +512,11 @@ fn parse_create_table_with_options() { name: None, option: ColumnOption::Options(vec![SqlOption::KeyValue { key: Ident::new("description"), - value: Expr::Value(Value::DoubleQuotedString( - "field x".to_string() - )), + value: Expr::Value( + Value::DoubleQuotedString("field x".to_string()).with_span( + Span::new(Location::new(1, 42), Location::new(1, 52)) + ) + ), },]) }, ] @@ -424,14 +524,15 @@ fn parse_create_table_with_options() { ColumnDef { name: Ident::new("y"), data_type: DataType::Bool, - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Options(vec![SqlOption::KeyValue { key: Ident::new("description"), - value: Expr::Value(Value::DoubleQuotedString( - "field y".to_string() - )), + value: Expr::Value( + Value::DoubleQuotedString("field y".to_string()).with_span( + Span::new(Location::new(1, 42), Location::new(1, 52)) + ) + ), },]) }] }, @@ -442,23 +543,32 @@ 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(number("1")), + value: Expr::Value( + number("1").with_span(Span::new( + Location::new(1, 42), + Location::new(1, 43) + )) + ), }, SqlOption::KeyValue { key: Ident::new("description"), - value: Expr::Value(Value::DoubleQuotedString( - "table option description".to_string() - )), + value: Expr::Value( + Value::DoubleQuotedString("table option description".to_string()) + .with_span(Span::new( + Location::new(1, 42), + Location::new(1, 52) + )) + ), }, ]) ), - (partition_by, cluster_by, options) + (partition_by, cluster_by, table_options) ) } _ => unreachable!(), @@ -473,6 +583,12 @@ fn parse_create_table_with_options() { r#"description = "table option description")"# ); bigquery().verified_stmt(sql); + + let sql = "CREATE TABLE foo (x INT64) OPTIONS()"; + bigquery().verified_stmt(sql); + + let sql = "CREATE TABLE db.schema.test (x INT64 OPTIONS(description = 'An optional INTEGER field')) OPTIONS()"; + bigquery().verified_stmt(sql); } #[test] @@ -480,7 +596,7 @@ fn parse_nested_data_types() { let sql = "CREATE TABLE table (x STRUCT, b BYTES(42)>, y ARRAY>)"; match bigquery_and_generic().one_statement_parses_to(sql, sql) { Statement::CreateTable(CreateTable { name, columns, .. }) => { - assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!(name, ObjectName::from(vec!["table".into()])); assert_eq!( columns, vec![ @@ -492,16 +608,17 @@ 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 ), - collation: None, options: vec![], }, ColumnDef { @@ -511,11 +628,11 @@ fn parse_nested_data_types() { vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }], StructBracketKind::AngleBrackets ), ))), - collation: None, options: vec![], }, ] @@ -525,56 +642,27 @@ 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 // syntax: (expr1, expr2 [, ... ]) - let sql = "SELECT (1, 2, 3), (1, 1.0, '123', true)"; + let sql = "SELECT (1, 2, 3), (1, 1.0, '123', true)"; // line 1, column 1 let select = bigquery().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Tuple(vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ]), expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Tuple(vec![ - Expr::Value(number("1")), - Expr::Value(number("1.0")), - Expr::Value(Value::SingleQuotedString("123".to_string())), - Expr::Value(Value::Boolean(true)) + Expr::value(number("1")), + Expr::value(number("1.0")), + Expr::Value(Value::SingleQuotedString("123".into()).with_empty_span()), + Expr::Value(Value::Boolean(true).with_empty_span()) ]), expr_from_projection(&select.projection[1]) ); @@ -590,9 +678,9 @@ fn parse_typeless_struct_syntax() { assert_eq!( &Expr::Struct { values: vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ], fields: Default::default() }, @@ -601,30 +689,35 @@ fn parse_typeless_struct_syntax() { assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("abc".to_string())),], + values: vec![Expr::Value( + Value::SingleQuotedString("abc".into()).with_empty_span() + )], fields: Default::default() }, expr_from_projection(&select.projection[1]) ); + assert_eq!( &Expr::Struct { values: vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::CompoundIdentifier(vec![Ident::from("t"), Ident::from("str_col")]), ], fields: Default::default() }, expr_from_projection(&select.projection[2]) ); + assert_eq!( &Expr::Struct { values: vec![ Expr::Named { - expr: Expr::Value(number("1")).into(), + expr: Expr::value(number("1")).into(), name: Ident::from("a") }, Expr::Named { - expr: Expr::Value(Value::SingleQuotedString("abc".to_string())).into(), + expr: Expr::Value(Value::SingleQuotedString("abc".into()).with_empty_span()) + .into(), name: Ident::from("b") }, ], @@ -632,6 +725,7 @@ fn parse_typeless_struct_syntax() { }, expr_from_projection(&select.projection[3]) ); + assert_eq!( &Expr::Struct { values: vec![Expr::Named { @@ -654,10 +748,11 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")),], + values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -665,7 +760,7 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!( &Expr::Struct { values: vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::CompoundIdentifier(vec![ Ident { value: "t".into(), @@ -686,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 { @@ -694,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, }, ] }, @@ -706,23 +803,26 @@ fn parse_typed_struct_syntax_bigquery() { value: "nested_col".into(), quote_style: None, span: Span::empty(), - }),], + })], fields: vec![ StructField { 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, }, ] }, @@ -738,20 +838,22 @@ fn parse_typed_struct_syntax_bigquery() { value: "nested_col".into(), quote_style: None, span: Span::empty(), - }),], + })], fields: vec![ StructField { field_name: Some("x".into()), 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, }, ] }, @@ -763,22 +865,24 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::Boolean(true)),], + 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]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedByteStringLiteral( - "abc".into() - )),], + values: vec![Expr::Value( + Value::SingleQuotedByteStringLiteral("abc".into()).with_empty_span() + )], 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]) @@ -789,45 +893,53 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(4, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::DoubleQuotedString( - "2011-05-05".to_string() - )),], + values: vec![Expr::Value( + Value::DoubleQuotedString("2011-05-05".into()).with_empty_span() + )], 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: "1999-01-01 01:23:34.45".to_string() - },], + 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]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5.0")),], + 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]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("1")),], + 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]) @@ -839,28 +951,41 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!( &Expr::Struct { values: vec![Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString("2".to_string()))), + value: Box::new(Expr::Value( + Value::SingleQuotedString("2".into()).with_empty_span() + )), leading_field: Some(DateTimeField::Hour), leading_precision: None, last_field: None, fractional_seconds_precision: None - }),], + })], 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: r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.to_string() - },], + 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]) @@ -871,23 +996,33 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::DoubleQuotedString("foo".to_string())),], + values: vec![Expr::Value( + Value::DoubleQuotedString("foo".into()).with_empty_span() + )], 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: "2008-12-25 15:30:00 America/Los_Angeles".to_string() - },], + 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]) @@ -895,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: "15:30:00".to_string() - },], + 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]) @@ -912,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: "1".to_string() - },], + 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: "1".to_string() - },], + 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]) @@ -943,15 +1093,17 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(1, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("1")), Expr::Value(number("2")),], + values: vec![Expr::value(number("1")), Expr::value(number("2")),], fields: vec![ StructField { field_name: Some("key".into()), field_type: DataType::Int64, + options: None, }, StructField { field_name: Some("value".into()), field_type: DataType::Int64, + options: None, }, ] }, @@ -969,10 +1121,11 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")),], + values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -980,7 +1133,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!( &Expr::Struct { values: vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::CompoundIdentifier(vec![ Ident { value: "t".into(), @@ -1001,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 { @@ -1009,40 +1163,13 @@ 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, }, ] }, expr_from_projection(&select.projection[1]) ); - assert_eq!( - &Expr::Struct { - values: vec![Expr::Identifier(Ident { - value: "nested_col".into(), - quote_style: None, - span: Span::empty(), - }),], - fields: vec![ - StructField { - field_name: Some("arr".into()), - field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( - DataType::Float64 - ))) - }, - StructField { - field_name: Some("str".into()), - field_type: DataType::Struct( - vec![StructField { - field_name: None, - field_type: DataType::Bool - }], - StructBracketKind::AngleBrackets - ) - }, - ] - }, - expr_from_projection(&select.projection[2]) - ); let sql = r#"SELECT STRUCT>(nested_col)"#; let select = bigquery_and_generic().verified_only_select(sql); @@ -1053,20 +1180,22 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { value: "nested_col".into(), quote_style: None, span: Span::empty(), - }),], + })], fields: vec![ StructField { field_name: Some("x".into()), 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, }, ] }, @@ -1078,22 +1207,24 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::Boolean(true)),], + 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]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedByteStringLiteral( - "abc".into() - )),], + values: vec![Expr::Value( + Value::SingleQuotedByteStringLiteral("abc".into()).with_empty_span() + )], 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]) @@ -1104,45 +1235,53 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(4, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString( - "2011-05-05".to_string() - )),], + values: vec![Expr::Value( + Value::SingleQuotedString("2011-05-05".into()).with_empty_span() + )], 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: "1999-01-01 01:23:34.45".to_string() - },], + 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]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5.0")),], + 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]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("1")),], + 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]) @@ -1154,28 +1293,41 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!( &Expr::Struct { values: vec![Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + value: Box::new(Expr::Value( + Value::SingleQuotedString("1".into()).with_empty_span() + )), leading_field: Some(DateTimeField::Month), leading_precision: None, last_field: None, fractional_seconds_precision: None - }),], + })], 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: r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.to_string() - },], + 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]) @@ -1186,23 +1338,33 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("foo".to_string())),], + values: vec![Expr::Value( + Value::SingleQuotedString("foo".into()).with_empty_span() + )], 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: "2008-12-25 15:30:00 America/Los_Angeles".to_string() - },], + 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]) @@ -1210,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: "15:30:00".to_string() - },], + 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]) @@ -1227,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: "1".to_string() - },], + 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: "1".to_string() - },], + 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]) @@ -1255,44 +1432,50 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { #[test] fn parse_typed_struct_with_field_name_bigquery() { - let sql = r#"SELECT STRUCT(5), STRUCT("foo")"#; + let sql = r#"SELECT STRUCT(5), STRUCT("foo")"#; // line 1, column 1 let select = bigquery().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")),], + 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]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::DoubleQuotedString("foo".to_string())),], + values: vec![Expr::Value( + Value::DoubleQuotedString("foo".into()).with_empty_span() + )], 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]) ); - let sql = r#"SELECT STRUCT(5, 5)"#; + let sql = r#"SELECT STRUCT(5, 5)"#; // line 1, column 1 let select = bigquery().verified_only_select(sql); assert_eq!(1, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")), Expr::Value(number("5")),], + values: vec![Expr::value(number("5")), Expr::value(number("5")),], 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, } ] }, @@ -1302,44 +1485,50 @@ fn parse_typed_struct_with_field_name_bigquery() { #[test] fn parse_typed_struct_with_field_name_bigquery_and_generic() { - let sql = r#"SELECT STRUCT(5), STRUCT('foo')"#; + let sql = r#"SELECT STRUCT(5), STRUCT('foo')"#; // line 1, column 1 let select = bigquery().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")),], + 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]) ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("foo".to_string())),], + values: vec![Expr::Value( + Value::SingleQuotedString("foo".into()).with_empty_span() + )], 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]) ); - let sql = r#"SELECT STRUCT(5, 5)"#; + let sql = r#"SELECT STRUCT(5, 5)"#; // line 1, column 1 let select = bigquery_and_generic().verified_only_select(sql); assert_eq!(1, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")), Expr::Value(number("5")),], + values: vec![Expr::value(number("5")), Expr::value(number("5")),], 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, } ] }, @@ -1370,7 +1559,7 @@ fn parse_table_identifiers() { assert_eq!( select.from, vec![TableWithJoins { - relation: table_from_name(ObjectName(expected)), + relation: table_from_name(ObjectName::from(expected)), joins: vec![] },] ); @@ -1512,7 +1701,10 @@ fn parse_hyphenated_table_identifiers() { ) .from[0] .relation, - table_from_name(ObjectName(vec![Ident::new("foo-123"), Ident::new("bar")])), + table_from_name(ObjectName::from(vec![ + Ident::new("foo-123"), + Ident::new("bar") + ])), ); assert_eq!( @@ -1531,31 +1723,29 @@ fn parse_hyphenated_table_identifiers() { ])) }) ); - - let error_sql = "select foo-bar.* from foo-bar"; - assert!(bigquery().parse_sql_statements(error_sql).is_err()); } #[test] fn parse_table_time_travel() { let version = "2023-08-18 23:08:18".to_string(); - let sql = format!("SELECT 1 FROM t1 FOR SYSTEM_TIME AS OF '{version}'"); + let sql = format!("SELECT 1 FROM t1 FOR SYSTEM_TIME AS OF '{version}'"); // line 1, column 1 let select = bigquery().verified_only_select(&sql); assert_eq!( select.from, vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t1")]), + name: ObjectName::from(vec![Ident::new("t1")]), alias: None, args: None, with_hints: vec![], version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( - Value::SingleQuotedString(version) + Value::SingleQuotedString(version).with_empty_span() ))), partitions: vec![], with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![] },] @@ -1586,7 +1776,7 @@ fn parse_join_constraint_unnest_alias() { with_ordinality: false, }, global: false, - join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), op: BinaryOperator::Eq, right: Box::new(Expr::Identifier("c2".into())), @@ -1617,22 +1807,24 @@ fn parse_merge() { let insert_action = MergeAction::Insert(MergeInsertExpr { columns: vec![Ident::new("product"), Ident::new("quantity")], kind: MergeInsertKind::Values(Values { + value_keyword: false, explicit_row: false, - rows: vec![vec![Expr::Value(number("1")), Expr::Value(number("2"))]], + rows: vec![vec![Expr::value(number("1")), Expr::value(number("2"))]], }), }); let update_action = MergeAction::Update { assignments: vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("a")])), - value: Expr::Value(number("1")), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("a")])), + value: Expr::value(number("1")), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("b")])), - value: Expr::Value(number("2")), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("b")])), + value: Expr::value(number("2")), }, ], }; + match bigquery_and_generic().verified_stmt(sql) { Statement::Merge { into, @@ -1640,11 +1832,12 @@ fn parse_merge() { source, on, clauses, + .. } => { assert!(!into); assert_eq!( TableFactor::Table { - name: ObjectName(vec![Ident::new("inventory")]), + name: ObjectName::from(vec![Ident::new("inventory")]), alias: Some(TableAlias { name: Ident::new("T"), columns: vec![], @@ -1656,12 +1849,13 @@ fn parse_merge() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, table ); assert_eq!( TableFactor::Table { - name: ObjectName(vec![Ident::new("newArrivals")]), + name: ObjectName::from(vec![Ident::new("newArrivals")]), alias: Some(TableAlias { name: Ident::new("S"), columns: vec![], @@ -1673,20 +1867,21 @@ fn parse_merge() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, source ); - assert_eq!(Expr::Value(Value::Boolean(false)), *on); + assert_eq!(Expr::Value(Value::Boolean(false).with_empty_span()), *on); assert_eq!( vec![ MergeClause { clause_kind: MergeClauseKind::NotMatched, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: insert_action.clone(), }, MergeClause { clause_kind: MergeClauseKind::NotMatchedByTarget, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: insert_action.clone(), }, MergeClause { @@ -1696,7 +1891,7 @@ fn parse_merge() { }, MergeClause { clause_kind: MergeClauseKind::NotMatchedBySource, - predicate: Some(Expr::Value(number("2"))), + predicate: Some(Expr::value(number("2"))), action: MergeAction::Delete }, MergeClause { @@ -1706,12 +1901,12 @@ fn parse_merge() { }, MergeClause { clause_kind: MergeClauseKind::NotMatchedBySource, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: update_action.clone(), }, MergeClause { clause_kind: MergeClauseKind::NotMatched, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: MergeAction::Insert(MergeInsertExpr { columns: vec![Ident::new("product"), Ident::new("quantity"),], kind: MergeInsertKind::Row, @@ -1727,7 +1922,7 @@ fn parse_merge() { }, MergeClause { clause_kind: MergeClauseKind::NotMatched, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: MergeAction::Insert(MergeInsertExpr { columns: vec![], kind: MergeInsertKind::Row @@ -1743,7 +1938,7 @@ fn parse_merge() { }, MergeClause { clause_kind: MergeClauseKind::Matched, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: MergeAction::Delete, }, MergeClause { @@ -1757,9 +1952,10 @@ fn parse_merge() { action: MergeAction::Insert(MergeInsertExpr { columns: vec![Ident::new("a"), Ident::new("b"),], kind: MergeInsertKind::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::Identifier(Ident::new("DEFAULT")), ]] }) @@ -1771,9 +1967,10 @@ fn parse_merge() { action: MergeAction::Insert(MergeInsertExpr { columns: vec![], kind: MergeInsertKind::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::Identifier(Ident::new("DEFAULT")), ]] }) @@ -1889,7 +2086,7 @@ fn parse_array_agg_func() { fn parse_big_query_declare() { for (sql, expected_names, expected_data_type, expected_assigned_expr) in [ ( - "DECLARE x INT64", + "DECLARE x INT64", // line 1, column 1 vec![Ident::new("x")], Some(DataType::Int64), None, @@ -1898,25 +2095,25 @@ fn parse_big_query_declare() { "DECLARE x INT64 DEFAULT 42", vec![Ident::new("x")], Some(DataType::Int64), - Some(DeclareAssignment::Default(Box::new(Expr::Value(number( - "42", - ))))), + Some(DeclareAssignment::Default(Box::new(Expr::Value( + number("42").with_empty_span(), + )))), ), ( "DECLARE x, y, z INT64 DEFAULT 42", vec![Ident::new("x"), Ident::new("y"), Ident::new("z")], Some(DataType::Int64), - Some(DeclareAssignment::Default(Box::new(Expr::Value(number( - "42", - ))))), + Some(DeclareAssignment::Default(Box::new(Expr::Value( + number("42").with_empty_span(), + )))), ), ( "DECLARE x DEFAULT 42", vec![Ident::new("x")], None, - Some(DeclareAssignment::Default(Box::new(Expr::Value(number( - "42", - ))))), + Some(DeclareAssignment::Default(Box::new(Expr::Value( + number("42").with_empty_span(), + )))), ), ] { match bigquery().verified_stmt(sql) { @@ -1974,12 +2171,12 @@ fn parse_map_access_expr() { AccessExpr::Subscript(Subscript::Index { index: Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Expr::Value(number("1")).into(), + expr: Expr::value(number("1")).into(), }, }), AccessExpr::Subscript(Subscript::Index { index: Expr::Function(Function { - name: ObjectName(vec![Ident::with_span( + name: ObjectName::from(vec![Ident::with_span( Span::new(Location::of(1, 11), Location::of(1, 22)), "safe_offset", )]), @@ -1987,7 +2184,7 @@ fn parse_map_access_expr() { args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - number("2"), + number("2").with_empty_span(), )))], clauses: vec![], }), @@ -2028,22 +2225,23 @@ fn test_bigquery_create_function() { assert_eq!( stmt, Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace: true, temporary: true, if_not_exists: false, - name: ObjectName(vec![ + name: ObjectName::from(vec![ Ident::new("project1"), Ident::new("mydataset"), Ident::new("myfunction"), ]), args: Some(vec![OperateFunctionArg::with_name("x", DataType::Float64),]), return_type: Some(DataType::Float64), - function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value(number( - "42" - )))), + function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value( + number("42").with_empty_span() + ))), options: Some(vec![SqlOption::KeyValue { key: Ident::new("x"), - value: Expr::Value(Value::SingleQuotedString("y".into())), + value: Expr::Value(Value::SingleQuotedString("y".into()).with_empty_span()), }]), behavior: None, using: None, @@ -2162,10 +2360,14 @@ fn test_bigquery_trim() { let select = bigquery().verified_only_select(sql_only_select); assert_eq!( &Expr::Trim { - expr: Box::new(Expr::Value(Value::SingleQuotedString("xyz".to_owned()))), + 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()))]), + trim_characters: Some(vec![Expr::Value( + Value::SingleQuotedString("a".to_owned()).with_empty_span() + )]), }, expr_from_projection(only(&select.projection)) ); @@ -2192,22 +2394,78 @@ fn parse_extract_weekday() { ); } +#[test] +fn bigquery_select_expr_star() { + bigquery() + .verified_only_select("SELECT STRUCT((SELECT foo FROM T WHERE true)).* FROM T"); + bigquery().verified_only_select("SELECT [STRUCT('foo')][0].* EXCEPT (foo) FROM T"); + bigquery().verified_only_select("SELECT myfunc()[0].* FROM T"); +} + #[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); } +#[test] +fn test_triple_quote_typed_strings() { + bigquery().verified_expr(r#"JSON '''{"foo":"bar's"}'''"#); + + let expr = bigquery().verified_expr(r#"JSON """{"foo":"bar's"}""""#); + assert_eq!( + Expr::TypedString(TypedString { + data_type: DataType::JSON, + value: ValueWithSpan { + value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), + expr + ); +} + #[test] fn test_array_agg() { bigquery_and_generic().verified_expr("ARRAY_AGG(state)"); @@ -2244,3 +2502,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 fed4308fc..44bfcda42 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; @@ -55,14 +55,15 @@ fn parse_map_access_expr() { "indexOf", [ Expr::Identifier(Ident::new("string_names")), - Expr::Value(Value::SingleQuotedString("endpoint".to_string())) + Expr::value(Value::SingleQuotedString("endpoint".to_string())) ] ), })], })], + exclude: None, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("foos")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("foos")])), joins: vec![], }], lateral_views: vec![], @@ -71,7 +72,7 @@ fn parse_map_access_expr() { left: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString("test".to_string()))), + right: Box::new(Expr::value(Value::SingleQuotedString("test".to_string()))), }), op: BinaryOperator::And, right: Box::new(BinaryOp { @@ -82,13 +83,13 @@ fn parse_map_access_expr() { "indexOf", [ Expr::Identifier(Ident::new("string_name")), - Expr::Value(Value::SingleQuotedString("app".to_string())) + Expr::value(Value::SingleQuotedString("app".to_string())) ] ), })], }), op: BinaryOperator::NotEq, - right: Box::new(Expr::Value(Value::SingleQuotedString("foo".to_string()))), + right: Box::new(Expr::value(Value::SingleQuotedString("foo".to_string()))), }), }), group_by: GroupByExpr::Expressions(vec![], vec![]), @@ -101,6 +102,7 @@ fn parse_map_access_expr() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }, select ); @@ -113,8 +115,8 @@ fn parse_array_expr() { assert_eq!( &Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("1".to_string())), - Expr::Value(Value::SingleQuotedString("2".to_string())), + Expr::value(Value::SingleQuotedString("1".to_string())), + Expr::value(Value::SingleQuotedString("2".to_string())), ], named: false, }), @@ -166,7 +168,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -185,7 +190,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -215,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", ); } @@ -234,12 +243,10 @@ fn parse_alter_table_attach_and_detach_partition() { match clickhouse_and_generic() .verified_stmt(format!("ALTER TABLE t0 {operation} PARTITION part").as_str()) { - Statement::AlterTable { - name, operations, .. - } => { - pretty_assertions::assert_eq!("t0", name.to_string()); + Statement::AlterTable(alter_table) => { + pretty_assertions::assert_eq!("t0", alter_table.name.to_string()); pretty_assertions::assert_eq!( - operations[0], + alter_table.operations[0], if operation == &"ATTACH" { AlterTableOperation::AttachPartition { partition: Partition::Expr(Identifier(Ident::new("part"))), @@ -257,9 +264,9 @@ fn parse_alter_table_attach_and_detach_partition() { match clickhouse_and_generic() .verified_stmt(format!("ALTER TABLE t1 {operation} PART part").as_str()) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { + }) => { pretty_assertions::assert_eq!("t1", name.to_string()); pretty_assertions::assert_eq!( operations[0], @@ -299,10 +306,10 @@ fn parse_alter_table_add_projection() { "ALTER TABLE t0 ADD PROJECTION IF NOT EXISTS my_name", " (SELECT a, b GROUP BY a ORDER BY b)", )) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { - assert_eq!(name, ObjectName(vec!["t0".into()])); + }) => { + assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( operations[0], @@ -319,12 +326,14 @@ fn parse_alter_table_add_projection() { vec![] )), order_by: Some(OrderBy { - exprs: vec![OrderByExpr { + kind: OrderByKind::Expressions(vec![OrderByExpr { expr: Identifier(Ident::new("b")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, - }], + }]), interpolate: None, }), } @@ -369,10 +378,10 @@ fn parse_alter_table_add_projection() { fn parse_alter_table_drop_projection() { match clickhouse_and_generic().verified_stmt("ALTER TABLE t0 DROP PROJECTION IF EXISTS my_name") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { - assert_eq!(name, ObjectName(vec!["t0".into()])); + }) => { + assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( operations[0], @@ -402,10 +411,10 @@ fn parse_alter_table_clear_and_materialize_projection() { format!("ALTER TABLE t0 {keyword} PROJECTION IF EXISTS my_name IN PARTITION p0",) .as_str(), ) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { - assert_eq!(name, ObjectName(vec!["t0".into()])); + }) => { + assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( operations[0], @@ -524,7 +533,6 @@ fn column_def(name: Ident, data_type: DataType) -> ColumnDef { ColumnDef { name, data_type, - collation: None, options: vec![], } } @@ -549,7 +557,7 @@ fn parse_clickhouse_data_types() { match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) { Statement::CreateTable(CreateTable { name, columns, .. }) => { - assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!(name, ObjectName::from(vec!["table".into()])); assert_eq!( columns, vec![ @@ -584,13 +592,13 @@ 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"); match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) { Statement::CreateTable(CreateTable { name, columns, .. }) => { - assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!(name, ObjectName::from(vec!["table".into()])); assert_eq!( columns, vec![ @@ -613,7 +621,6 @@ fn parse_create_table_with_nullable() { ColumnDef { name: "d".into(), data_type: DataType::Date32, - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Null @@ -639,7 +646,7 @@ fn parse_create_table_with_nested_data_types() { match clickhouse().one_statement_parses_to(sql, "") { Statement::CreateTable(CreateTable { name, columns, .. }) => { - assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!(name, ObjectName::from(vec!["table".into()])); assert_eq!( columns, vec![ @@ -657,7 +664,6 @@ fn parse_create_table_with_nested_data_types() { DataType::LowCardinality(Box::new(DataType::String(None))) ) ]), - collation: None, options: vec![], }, ColumnDef { @@ -666,15 +672,16 @@ 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, } ]) ))), - collation: None, options: vec![], }, ColumnDef { @@ -683,15 +690,16 @@ 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, }, ]), - collation: None, options: vec![], }, ColumnDef { @@ -700,7 +708,6 @@ fn parse_create_table_with_nested_data_types() { Box::new(DataType::String(None)), Box::new(DataType::UInt16) ), - collation: None, options: vec![], }, ] @@ -714,14 +721,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, .. @@ -732,30 +739,35 @@ fn parse_create_table_with_primary_key() { ColumnDef { name: Ident::with_quote('`', "i"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ColumnDef { name: Ident::with_quote('`', "k"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ], 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(vec![Ident::new(name)])); + assert_eq!(actual.name, ObjectName::from(vec![Ident::new(name)])); assert_eq!( actual.args, FunctionArguments::List(FunctionArgumentList { @@ -800,7 +812,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, .. }) => { @@ -810,11 +822,10 @@ fn parse_create_table_with_variant_default_expressions() { ColumnDef { name: Ident::new("a"), data_type: DataType::Datetime(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Materialized(Expr::Function(Function { - name: ObjectName(vec![Ident::new("now")]), + name: ObjectName::from(vec![Ident::new("now")]), uses_odbc_syntax: false, args: FunctionArguments::List(FunctionArgumentList { args: vec![], @@ -832,11 +843,10 @@ fn parse_create_table_with_variant_default_expressions() { ColumnDef { name: Ident::new("b"), data_type: DataType::Datetime(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Ephemeral(Some(Expr::Function(Function { - name: ObjectName(vec![Ident::new("now")]), + name: ObjectName::from(vec![Ident::new("now")]), uses_odbc_syntax: false, args: FunctionArguments::List(FunctionArgumentList { args: vec![], @@ -854,7 +864,6 @@ fn parse_create_table_with_variant_default_expressions() { ColumnDef { name: Ident::new("c"), data_type: DataType::Datetime(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Ephemeral(None) @@ -863,11 +872,10 @@ fn parse_create_table_with_variant_default_expressions() { ColumnDef { name: Ident::new("d"), data_type: DataType::String(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Alias(Expr::Function(Function { - name: ObjectName(vec![Ident::new("toString")]), + name: ObjectName::from(vec![Ident::new("toString")]), uses_odbc_syntax: false, args: FunctionArguments::List(FunctionArgumentList { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( @@ -894,34 +902,34 @@ fn parse_create_table_with_variant_default_expressions() { #[test] fn parse_create_view_with_fields_data_types() { match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) { - Statement::CreateView { name, columns, .. } => { - assert_eq!(name, ObjectName(vec!["v".into()])); + Statement::CreateView(CreateView { name, columns, .. }) => { + assert_eq!(name, ObjectName::from(vec!["v".into()])); assert_eq!( columns, vec![ ViewColumnDef { name: "i".into(), data_type: Some(DataType::Custom( - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "int".into(), quote_style: Some('"'), span: Span::empty(), }]), vec![] )), - options: None + options: None, }, ViewColumnDef { name: "f".into(), data_type: Some(DataType::Custom( - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "String".into(), quote_style: Some('"'), span: Span::empty(), }]), vec![] )), - options: None + options: None, }, ] ); @@ -950,42 +958,113 @@ fn parse_limit_by() { clickhouse_and_generic().verified_stmt( r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC LIMIT 1 BY asset, toStartOfDay(created_at)"#, ); + clickhouse_and_generic().parse_sql_statements( + r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC BY asset, toStartOfDay(created_at)"#, + ).expect_err("BY without LIMIT"); + clickhouse_and_generic() + .parse_sql_statements("SELECT * FROM T OFFSET 5 BY foo") + .expect_err("BY with OFFSET but without LIMIT"); } #[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] @@ -1022,17 +1101,15 @@ fn parse_select_parametric_function() { assert_eq!(parameters.args.len(), 2); assert_eq!( parameters.args[0], - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Number( - "0.5".parse().unwrap(), - false - )))) + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (Value::Number("0.5".parse().unwrap(), false)).with_empty_span() + ))) ); assert_eq!( parameters.args[1], - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Number( - "0.6".parse().unwrap(), - false - )))) + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (Value::Number("0.6".parse().unwrap(), false)).with_empty_span() + ))) ); } _ => unreachable!(), @@ -1065,61 +1142,6 @@ fn parse_create_materialized_view() { clickhouse_and_generic().verified_stmt(sql); } -#[test] -fn parse_group_by_with_modifier() { - let clauses = ["x", "a, b", "ALL"]; - let modifiers = [ - "WITH ROLLUP", - "WITH CUBE", - "WITH TOTALS", - "WITH ROLLUP WITH CUBE", - ]; - let expected_modifiers = [ - vec![GroupByWithModifier::Rollup], - vec![GroupByWithModifier::Cube], - vec![GroupByWithModifier::Totals], - vec![GroupByWithModifier::Rollup, GroupByWithModifier::Cube], - ]; - for clause in &clauses { - for (modifier, expected_modifier) in modifiers.iter().zip(expected_modifiers.iter()) { - let sql = format!("SELECT * FROM t GROUP BY {clause} {modifier}"); - match clickhouse_and_generic().verified_stmt(&sql) { - Statement::Query(query) => { - let group_by = &query.body.as_select().unwrap().group_by; - if clause == &"ALL" { - assert_eq!(group_by, &GroupByExpr::All(expected_modifier.to_vec())); - } else { - assert_eq!( - group_by, - &GroupByExpr::Expressions( - clause - .split(", ") - .map(|c| Identifier(Ident::new(c))) - .collect(), - expected_modifier.to_vec() - ) - ); - } - } - _ => unreachable!(), - } - } - } - - // invalid cases - let invalid_cases = [ - "SELECT * FROM t GROUP BY x WITH", - "SELECT * FROM t GROUP BY x WITH ROLLUP CUBE", - "SELECT * FROM t GROUP BY x WITH WITH ROLLUP", - "SELECT * FROM t GROUP BY WITH ROLLUP", - ]; - for sql in invalid_cases { - clickhouse_and_generic() - .parse_sql_statements(sql) - .expect_err("Expected: one of ROLLUP or CUBE or TOTALS, found: WITH"); - } -} - #[test] fn parse_select_order_by_with_fill_interpolate() { let sql = "SELECT id, fname, lname FROM customer WHERE id < 5 \ @@ -1131,42 +1153,53 @@ fn parse_select_order_by_with_fill_interpolate() { let select = clickhouse().verified_query(sql); assert_eq!( OrderBy { - exprs: vec![ + kind: OrderByKind::Expressions(vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), - asc: Some(true), - nulls_first: Some(true), + options: OrderByOptions { + asc: Some(true), + nulls_first: Some(true), + }, with_fill: Some(WithFill { - from: Some(Expr::Value(number("10"))), - to: Some(Expr::Value(number("20"))), - step: Some(Expr::Value(number("2"))), + from: Some(Expr::value(number("10"))), + to: Some(Expr::value(number("20"))), + step: Some(Expr::value(number("2"))), }), }, OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), - asc: Some(false), - nulls_first: Some(false), + options: OrderByOptions { + asc: Some(false), + nulls_first: Some(false), + }, with_fill: Some(WithFill { - from: Some(Expr::Value(number("30"))), - to: Some(Expr::Value(number("40"))), - step: Some(Expr::Value(number("3"))), + from: Some(Expr::value(number("30"))), + to: Some(Expr::value(number("40"))), + step: Some(Expr::value(number("3"))), }), }, - ], + ]), interpolate: Some(Interpolate { exprs: Some(vec![InterpolateExpr { column: Ident::new("col1"), expr: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("col1"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), }]) }) }, select.order_by.expect("ORDER BY expected") ); - assert_eq!(Some(Expr::Value(number("2"))), select.limit); + assert_eq!( + select.limit_clause, + Some(LimitClause::LimitOffset { + limit: Some(Expr::value(number("2"))), + offset: None, + limit_by: vec![] + }) + ); } #[test] @@ -1207,11 +1240,15 @@ fn parse_with_fill() { let select = clickhouse().verified_query(sql); assert_eq!( Some(WithFill { - from: Some(Expr::Value(number("10"))), - to: Some(Expr::Value(number("20"))), - step: Some(Expr::Value(number("2"))), - }), - select.order_by.expect("ORDER BY expected").exprs[0].with_fill + from: Some(Expr::value(number("10"))), + to: Some(Expr::value(number("20"))), + step: Some(Expr::value(number("2"))), + }) + .as_ref(), + match select.order_by.expect("ORDER BY expected").kind { + OrderByKind::Expressions(ref exprs) => exprs[0].with_fill.as_ref(), + _ => None, + } ); } @@ -1246,7 +1283,7 @@ fn parse_interpolate_body_with_columns() { expr: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("col1"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), }, InterpolateExpr { @@ -1258,12 +1295,17 @@ fn parse_interpolate_body_with_columns() { expr: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("col4"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("4"))), + right: Box::new(Expr::value(number("4"))), }), }, ]) - }), - select.order_by.expect("ORDER BY expected").interpolate + }) + .as_ref(), + select + .order_by + .expect("ORDER BY expected") + .interpolate + .as_ref() ); } @@ -1272,8 +1314,12 @@ fn parse_interpolate_without_body() { let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL INTERPOLATE"; let select = clickhouse().verified_query(sql); assert_eq!( - Some(Interpolate { exprs: None }), - select.order_by.expect("ORDER BY expected").interpolate + Some(Interpolate { exprs: None }).as_ref(), + select + .order_by + .expect("ORDER BY expected") + .interpolate + .as_ref() ); } @@ -1284,8 +1330,13 @@ fn parse_interpolate_with_empty_body() { assert_eq!( Some(Interpolate { exprs: Some(vec![]) - }), - select.order_by.expect("ORDER BY expected").interpolate + }) + .as_ref(), + select + .order_by + .expect("ORDER BY expected") + .interpolate + .as_ref() ); } @@ -1299,7 +1350,9 @@ fn test_prewhere() { Some(&BinaryOp { left: Box::new(Identifier(Ident::new("x"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), }) ); let selection = query.as_ref().body.as_select().unwrap().selection.as_ref(); @@ -1308,7 +1361,9 @@ fn test_prewhere() { Some(&BinaryOp { left: Box::new(Identifier(Ident::new("y"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("2".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("2".parse().unwrap(), false)).with_empty_span() + )), }) ); } @@ -1324,13 +1379,17 @@ fn test_prewhere() { left: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("x"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), }), op: BinaryOperator::And, right: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("y"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("2".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("2".parse().unwrap(), false)).with_empty_span() + )), }), }) ); @@ -1354,16 +1413,16 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - clickhouse().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + clickhouse().verified_stmt(&format!("USE {object_name}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); 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)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + clickhouse().verified_stmt(&format!("USE {quote}{object_name}{quote}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), )]))) @@ -1376,7 +1435,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" { @@ -1438,10 +1497,9 @@ fn parse_create_table_on_commit_and_as_query() { assert_eq!(on_commit, Some(OnCommit::PreserveRows)); assert_eq!( query.unwrap().body.as_select().unwrap().projection, - vec![UnnamedExpr(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - )))] + vec![UnnamedExpr(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + ))] ); } _ => unreachable!(), @@ -1454,11 +1512,11 @@ fn parse_freeze_and_unfreeze_partition() { for operation_name in &["FREEZE", "UNFREEZE"] { let sql = format!("ALTER TABLE t {operation_name} PARTITION '2024-08-14'"); - let expected_partition = Partition::Expr(Expr::Value(Value::SingleQuotedString( - "2024-08-14".to_string(), - ))); + let expected_partition = Partition::Expr(Expr::Value( + Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(), + )); match clickhouse_and_generic().verified_stmt(&sql) { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!(operations.len(), 1); let expected_operation = if operation_name == &"FREEZE" { AlterTableOperation::FreezePartition { @@ -1482,11 +1540,11 @@ fn parse_freeze_and_unfreeze_partition() { let sql = format!("ALTER TABLE t {operation_name} PARTITION '2024-08-14' WITH NAME 'hello'"); match clickhouse_and_generic().verified_stmt(&sql) { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!(operations.len(), 1); - let expected_partition = Partition::Expr(Expr::Value(Value::SingleQuotedString( - "2024-08-14".to_string(), - ))); + let expected_partition = Partition::Expr(Expr::Value( + Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(), + )); let expected_operation = if operation_name == &"FREEZE" { AlterTableOperation::FreezePartition { partition: expected_partition, @@ -1560,11 +1618,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")), }, ]), }, @@ -1585,11 +1643,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")), }, ]), }, @@ -1599,7 +1657,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() @@ -1646,6 +1703,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 07a30bc08..e85a8ec6d 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] @@ -55,30 +58,67 @@ use sqlparser::ast::Expr::{Identifier, UnaryOp}; use sqlparser::ast::Value::Number; use sqlparser::test_utils::all_dialects_except; +#[test] +fn parse_numeric_literal_underscore() { + let dialects = all_dialects_where(|d| d.supports_numeric_literal_underscores()); + + let canonical = if cfg!(feature = "bigdecimal") { + "SELECT 10000" + } else { + "SELECT 10_000" + }; + + let select = dialects.verified_only_select_with_canonical("SELECT 10_000", canonical); + + assert_eq!( + select.projection, + vec![UnnamedExpr(Expr::Value( + (number("10_000")).with_empty_span() + ))] + ); +} + +#[test] +fn parse_function_object_name() { + let select = verified_only_select("SELECT a.b.c.d(1, 2, 3) FROM T"); + let Expr::Function(func) = expr_from_projection(&select.projection[0]) else { + unreachable!() + }; + assert_eq!( + ObjectName::from( + ["a", "b", "c", "d"] + .into_iter() + .map(Ident::new) + .collect::>() + ), + func.name, + ); +} + #[test] fn parse_insert_values() { let row = vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ]; let rows1 = vec![row.clone()]; let rows2 = vec![row.clone(), row]; let sql = "INSERT customer VALUES (1, 2, 3)"; - check_one(sql, "customer", &[], &rows1); + check_one(sql, "customer", &[], &rows1, false); let sql = "INSERT INTO customer VALUES (1, 2, 3)"; - check_one(sql, "customer", &[], &rows1); + check_one(sql, "customer", &[], &rows1, false); let sql = "INSERT INTO customer VALUES (1, 2, 3), (1, 2, 3)"; - check_one(sql, "customer", &[], &rows2); + check_one(sql, "customer", &[], &rows2, false); let sql = "INSERT INTO public.customer VALUES (1, 2, 3)"; - check_one(sql, "public.customer", &[], &rows1); + check_one(sql, "public.customer", &[], &rows1, false); let sql = "INSERT INTO db.public.customer VALUES (1, 2, 3)"; - check_one(sql, "db.public.customer", &[], &rows1); + check_one(sql, "db.public.customer", &[], &rows1, false); let sql = "INSERT INTO public.customer (id, name, active) VALUES (1, 2, 3)"; check_one( @@ -86,6 +126,16 @@ fn parse_insert_values() { "public.customer", &["id".to_string(), "name".to_string(), "active".to_string()], &rows1, + false, + ); + + let sql = r"INSERT INTO t (id, name, active) VALUE (1, 2, 3)"; + check_one( + sql, + "t", + &["id".to_string(), "name".to_string(), "active".to_string()], + &rows1, + true, ); fn check_one( @@ -93,6 +143,7 @@ fn parse_insert_values() { expected_table_name: &str, expected_columns: &[String], expected_rows: &[Vec], + expected_value_keyword: bool, ) { match verified_stmt(sql) { Statement::Insert(Insert { @@ -107,8 +158,13 @@ fn parse_insert_values() { assert_eq!(column, &Ident::new(expected_columns[index].clone())); } match *source.body { - SetExpr::Values(Values { rows, .. }) => { - assert_eq!(rows.as_slice(), expected_rows) + SetExpr::Values(Values { + rows, + value_keyword, + .. + }) => { + assert_eq!(rows.as_slice(), expected_rows); + assert!(value_keyword == expected_value_keyword); } _ => unreachable!(), } @@ -160,7 +216,7 @@ fn parse_insert_default_values() { assert_eq!(source, None); assert_eq!( table_name, - TableObject::TableName(ObjectName(vec!["test_table".into()])) + TableObject::TableName(ObjectName::from(vec!["test_table".into()])) ); } _ => unreachable!(), @@ -188,7 +244,7 @@ fn parse_insert_default_values() { assert_eq!(source, None); assert_eq!( table_name, - TableObject::TableName(ObjectName(vec!["test_table".into()])) + TableObject::TableName(ObjectName::from(vec!["test_table".into()])) ); } _ => unreachable!(), @@ -216,7 +272,7 @@ fn parse_insert_default_values() { assert_eq!(source, None); assert_eq!( table_name, - TableObject::TableName(ObjectName(vec!["test_table".into()])) + TableObject::TableName(ObjectName::from(vec!["test_table".into()])) ); } _ => unreachable!(), @@ -253,8 +309,13 @@ fn parse_insert_default_values() { #[test] fn parse_insert_select_returning() { - verified_stmt("INSERT INTO t SELECT 1 RETURNING 2"); - let stmt = verified_stmt("INSERT INTO t SELECT x RETURNING x AS y"); + // Dialects that support `RETURNING` as a column identifier do + // not support this syntax. + let dialects = + all_dialects_where(|d| !d.is_column_alias(&Keyword::RETURNING, &mut Parser::new(d))); + + dialects.verified_stmt("INSERT INTO t SELECT 1 RETURNING 2"); + let stmt = dialects.verified_stmt("INSERT INTO t SELECT x RETURNING x AS y"); match stmt { Statement::Insert(Insert { returning: Some(ret), @@ -265,6 +326,27 @@ fn parse_insert_select_returning() { } } +#[test] +fn parse_insert_select_from_returning() { + let sql = "INSERT INTO table1 SELECT * FROM table2 RETURNING id"; + match verified_stmt(sql) { + Statement::Insert(Insert { + table: TableObject::TableName(table_name), + source: Some(source), + returning: Some(returning), + .. + }) => { + assert_eq!("table1", table_name.to_string()); + assert!(matches!(*source.body, SetExpr::Select(_))); + assert_eq!( + returning, + vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))),] + ); + } + bad_stmt => unreachable!("Expected valid insert, got {:?}", bad_stmt), + } +} + #[test] fn parse_returning_as_column_alias() { verified_stmt("SELECT 1 AS RETURNING"); @@ -311,27 +393,27 @@ fn parse_insert_sqlite() { fn parse_update() { let sql = "UPDATE t SET a = 1, b = 2, c = 3 WHERE d"; match verified_stmt(sql) { - Statement::Update { + Statement::Update(Update { table, assignments, selection, .. - } => { + }) => { assert_eq!(table.to_string(), "t".to_string()); assert_eq!( assignments, vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["a".into()])), - value: Expr::Value(number("1")), + target: AssignmentTarget::ColumnName(ObjectName::from(vec!["a".into()])), + value: Expr::value(number("1")), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["b".into()])), - value: Expr::Value(number("2")), + target: AssignmentTarget::ColumnName(ObjectName::from(vec!["b".into()])), + value: Expr::value(number("2")), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["c".into()])), - value: Expr::Value(number("3")), + target: AssignmentTarget::ColumnName(ObjectName::from(vec!["c".into()])), + value: Expr::value(number("3")), }, ] ); @@ -373,16 +455,16 @@ fn parse_update_set_from() { let stmt = dialects.verified_stmt(sql); assert_eq!( stmt, - Statement::Update { + Statement::Update(Update { table: TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("t1")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], }, assignments: vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("name")])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("name")])), value: Expr::CompoundIdentifier(vec![Ident::new("t2"), Ident::new("name")]) }], - from: Some(UpdateTableFromKind::AfterSet(TableWithJoins { + from: Some(UpdateTableFromKind::AfterSet(vec![TableWithJoins { relation: TableFactor::Derived { lateral: false, subquery: Box::new(Query { @@ -396,9 +478,10 @@ 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(vec![Ident::new("t1")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], }], lateral_views: vec![], @@ -417,16 +500,16 @@ fn parse_update_set_from() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), alias: Some(TableAlias { name: Ident::new("t2"), @@ -434,7 +517,7 @@ fn parse_update_set_from() { }) }, joins: vec![] - })), + }])), selection: Some(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("t1"), @@ -448,26 +531,31 @@ fn parse_update_set_from() { }), returning: None, or: None, - } + limit: None + }) ); + + let sql = "UPDATE T SET a = b FROM U, (SELECT foo FROM V) AS W WHERE 1 = 1"; + dialects.verified_stmt(sql); } #[test] fn parse_update_with_table_alias() { let sql = "UPDATE users AS u SET u.username = 'new_user' WHERE u.username = 'old_user'"; match verified_stmt(sql) { - Statement::Update { + Statement::Update(Update { table, assignments, from: _from, selection, returning, or: None, - } => { + limit: None, + }) => { assert_eq!( TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("users")]), + name: ObjectName::from(vec![Ident::new("users")]), alias: Some(TableAlias { name: Ident::new("u"), columns: vec![], @@ -479,6 +567,7 @@ fn parse_update_with_table_alias() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![], }, @@ -486,11 +575,13 @@ fn parse_update_with_table_alias() { ); assert_eq!( vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![ + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ Ident::new("u"), Ident::new("username") ])), - value: Expr::Value(Value::SingleQuotedString("new_user".to_string())), + value: Expr::Value( + (Value::SingleQuotedString("new_user".to_string())).with_empty_span() + ), }], assignments ); @@ -501,9 +592,9 @@ fn parse_update_with_table_alias() { Ident::new("username"), ])), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "old_user".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("old_user".to_string())).with_empty_span() + )), }), selection ); @@ -516,7 +607,7 @@ fn parse_update_with_table_alias() { #[test] fn parse_update_or() { let expect_or_clause = |sql: &str, expected_action: SqliteOnConflict| match verified_stmt(sql) { - Statement::Update { or, .. } => assert_eq!(or, Some(expected_action)), + Statement::Update(Update { or, .. }) => assert_eq!(or, Some(expected_action)), other => unreachable!("Expected update with or, got {:?}", other), }; expect_or_clause( @@ -556,7 +647,7 @@ fn parse_select_with_table_alias() { select.from, vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("lineitem")]), + name: ObjectName::from(vec![Ident::new("lineitem")]), alias: Some(TableAlias { name: Ident::new("l"), columns: vec![ @@ -572,6 +663,7 @@ fn parse_select_with_table_alias() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![], }] @@ -607,7 +699,7 @@ fn parse_delete_statement() { .. }) => { assert_eq!( - table_from_name(ObjectName(vec![Ident::with_quote('"', "table")])), + table_from_name(ObjectName::from(vec![Ident::with_quote('"', "table")])), from[0].relation ); } @@ -638,22 +730,22 @@ fn parse_delete_statement_for_multi_tables() { .. }) => { assert_eq!( - ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), + ObjectName::from(vec![Ident::new("schema1"), Ident::new("table1")]), tables[0] ); assert_eq!( - ObjectName(vec![Ident::new("schema2"), Ident::new("table2")]), + ObjectName::from(vec![Ident::new("schema2"), Ident::new("table2")]), tables[1] ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema1"), Ident::new("table1") ])), from[0].relation ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema2"), Ident::new("table2") ])), @@ -674,28 +766,28 @@ fn parse_delete_statement_for_multi_tables_with_using() { .. }) => { assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema1"), Ident::new("table1") ])), from[0].relation ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema2"), Ident::new("table2") ])), from[1].relation ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema1"), Ident::new("table1") ])), using[0].relation ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema2"), Ident::new("table2") ])), @@ -721,7 +813,7 @@ fn parse_where_delete_statement() { .. }) => { assert_eq!( - table_from_name(ObjectName(vec![Ident::new("foo")])), + table_from_name(ObjectName::from(vec![Ident::new("foo")])), from[0].relation, ); @@ -730,7 +822,7 @@ fn parse_where_delete_statement() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("name"))), op: Eq, - right: Box::new(Expr::Value(number("5"))), + right: Box::new(Expr::value(number("5"))), }, selection.unwrap(), ); @@ -756,7 +848,7 @@ fn parse_where_delete_with_alias_statement() { }) => { assert_eq!( TableFactor::Table { - name: ObjectName(vec![Ident::new("basket")]), + name: ObjectName::from(vec![Ident::new("basket")]), alias: Some(TableAlias { name: Ident::new("a"), columns: vec![], @@ -768,13 +860,14 @@ fn parse_where_delete_with_alias_statement() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, from[0].relation, ); assert_eq!( Some(vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("basket")]), + name: ObjectName::from(vec![Ident::new("basket")]), alias: Some(TableAlias { name: Ident::new("b"), columns: vec![], @@ -786,6 +879,7 @@ fn parse_where_delete_with_alias_statement() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![], }]), @@ -827,7 +921,12 @@ fn parse_simple_select() { assert!(select.distinct.is_none()); assert_eq!(3, select.projection.len()); let select = verified_query(sql); - assert_eq!(Some(Expr::Value(number("5"))), select.limit); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("5"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), select.limit_clause); } #[test] @@ -835,14 +934,31 @@ fn parse_limit() { verified_stmt("SELECT * FROM user LIMIT 1"); } +#[test] +fn parse_invalid_limit_by() { + all_dialects() + .parse_sql_statements("SELECT * FROM user BY name") + .expect_err("BY without LIMIT"); +} + #[test] fn parse_limit_is_not_an_alias() { // In dialects supporting LIMIT it shouldn't be parsed as a table alias let ast = verified_query("SELECT id FROM customer LIMIT 1"); - assert_eq!(Some(Expr::Value(number("1"))), ast.limit); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("1"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), ast.limit_clause); let ast = verified_query("SELECT 1 LIMIT 5"); - assert_eq!(Some(Expr::Value(number("5"))), ast.limit); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("5"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), ast.limit_clause); } #[test] @@ -884,6 +1000,44 @@ fn parse_select_distinct_tuple() { ); } +#[test] +fn parse_outer_join_operator() { + let dialects = all_dialects_where(|d| d.supports_outer_join_operator()); + + let select = dialects.verified_only_select("SELECT 1 FROM T WHERE a = b (+)"); + assert_eq!( + select.selection, + Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::OuterJoin(Box::new(Expr::Identifier(Ident::new("b"))))) + }) + ); + + let select = dialects.verified_only_select("SELECT 1 FROM T WHERE t1.c1 = t2.c2.d3 (+)"); + assert_eq!( + select.selection, + Some(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("t1"), + Ident::new("c1") + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::OuterJoin(Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("t2"), + Ident::new("c2"), + Ident::new("d3"), + ])))) + }) + ); + + let res = dialects.parse_sql_statements("SELECT 1 FROM T WHERE 1 = 2 (+)"); + assert_eq!( + ParserError::ParserError("Expected: column identifier before (+), found: 2".to_string()), + res.unwrap_err() + ); +} + #[test] fn parse_select_distinct_on() { let sql = "SELECT DISTINCT ON (album_id) name FROM track ORDER BY album_id, milliseconds"; @@ -941,7 +1095,7 @@ fn parse_select_into() { temporary: false, unlogged: false, table: false, - name: ObjectName(vec![Ident::new("table0")]), + name: ObjectName::from(vec![Ident::new("table0")]), }, only(&select.into) ); @@ -974,7 +1128,7 @@ fn parse_select_wildcard() { let select = verified_only_select(sql); assert_eq!( &SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("foo")]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("foo")])), WildcardAdditionalOptions::default() ), only(&select.projection) @@ -984,7 +1138,10 @@ fn parse_select_wildcard() { let select = verified_only_select(sql); assert_eq!( &SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("myschema"), Ident::new("mytable"),]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![ + Ident::new("myschema"), + Ident::new("mytable"), + ])), WildcardAdditionalOptions::default(), ), only(&select.projection) @@ -1019,7 +1176,7 @@ fn parse_column_aliases() { } = only(&select.projection) { assert_eq!(&BinaryOperator::Plus, op); - assert_eq!(&Expr::Value(number("1")), right.as_ref()); + assert_eq!(&Expr::value(number("1")), right.as_ref()); assert_eq!(&Ident::new("newname"), alias); } else { panic!("Expected: ExprWithAlias") @@ -1029,6 +1186,88 @@ fn parse_column_aliases() { one_statement_parses_to("SELECT a.col + 1 newname FROM foo AS a", sql); } +#[test] +fn parse_select_expr_star() { + let dialects = all_dialects_where(|d| d.supports_select_expr_star()); + + // Identifier wildcard expansion. + let select = dialects.verified_only_select("SELECT foo.bar.* FROM T"); + let SelectItem::QualifiedWildcard(SelectItemQualifiedWildcardKind::ObjectName(object_name), _) = + only(&select.projection) + else { + unreachable!( + "expected wildcard select item: got {:?}", + &select.projection[0] + ) + }; + assert_eq!( + &ObjectName::from( + ["foo", "bar"] + .into_iter() + .map(Ident::new) + .collect::>() + ), + object_name + ); + + // Arbitrary compound expression with wildcard expansion. + let select = dialects.verified_only_select("SELECT foo - bar.* FROM T"); + let SelectItem::QualifiedWildcard( + SelectItemQualifiedWildcardKind::Expr(Expr::BinaryOp { left, op, right }), + _, + ) = only(&select.projection) + else { + unreachable!( + "expected wildcard select item: got {:?}", + &select.projection[0] + ) + }; + let (Expr::Identifier(left), BinaryOperator::Minus, Expr::Identifier(right)) = + (left.as_ref(), op, right.as_ref()) + else { + unreachable!("expected binary op expr: got {:?}", &select.projection[0]) + }; + assert_eq!(&Ident::new("foo"), left); + assert_eq!(&Ident::new("bar"), right); + + // Arbitrary expression wildcard expansion. + let select = dialects.verified_only_select("SELECT myfunc().foo.* FROM T"); + let SelectItem::QualifiedWildcard( + SelectItemQualifiedWildcardKind::Expr(Expr::CompoundFieldAccess { root, access_chain }), + _, + ) = only(&select.projection) + else { + unreachable!("expected wildcard expr: got {:?}", &select.projection[0]) + }; + assert!(matches!(root.as_ref(), Expr::Function(_))); + assert_eq!(1, access_chain.len()); + assert!(matches!( + &access_chain[0], + AccessExpr::Dot(Expr::Identifier(_)) + )); + + dialects.one_statement_parses_to( + "SELECT 2. * 3 FROM T", + #[cfg(feature = "bigdecimal")] + "SELECT 2 * 3 FROM T", + #[cfg(not(feature = "bigdecimal"))] + "SELECT 2. * 3 FROM T", + ); + dialects.verified_only_select("SELECT myfunc().* FROM T"); + + // Invalid + let res = dialects.parse_sql_statements("SELECT foo.*.* FROM T"); + assert_eq!( + 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] fn test_eof_after_as() { let res = parse_sql_statements("SELECT foo AS"); @@ -1061,7 +1300,7 @@ fn parse_select_count_wildcard() { let select = verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("COUNT")]), + name: ObjectName::from(vec![Ident::new("COUNT")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -1084,7 +1323,7 @@ fn parse_select_count_distinct() { let select = verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("COUNT")]), + name: ObjectName::from(vec![Ident::new("COUNT")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -1168,7 +1407,7 @@ fn parse_null_in_select() { let sql = "SELECT NULL"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Null), + &Expr::Value((Value::Null).with_empty_span()), expr_from_projection(only(&select.projection)), ); } @@ -1204,18 +1443,18 @@ fn parse_exponent_in_select() -> Result<(), ParserError> { assert_eq!( &vec![ - SelectItem::UnnamedExpr(Expr::Value(number("10e-20"))), - SelectItem::UnnamedExpr(Expr::Value(number("1e3"))), - SelectItem::UnnamedExpr(Expr::Value(number("1e+3"))), + SelectItem::UnnamedExpr(Expr::Value((number("10e-20")).with_empty_span())), + SelectItem::UnnamedExpr(Expr::value(number("1e3"))), + SelectItem::UnnamedExpr(Expr::Value((number("1e+3")).with_empty_span())), SelectItem::ExprWithAlias { - expr: Expr::Value(number("1e3")), + expr: Expr::value(number("1e3")), alias: Ident::new("a") }, SelectItem::ExprWithAlias { - expr: Expr::Value(number("1")), + expr: Expr::value(number("1")), alias: Ident::new("e") }, - SelectItem::UnnamedExpr(Expr::Value(number("0.5e2"))), + SelectItem::UnnamedExpr(Expr::value(number("0.5e2"))), ], &select.projection ); @@ -1249,9 +1488,9 @@ fn parse_escaped_single_quote_string_predicate_with_escape() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("salary"))), op: NotEq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "Jim's salary".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("Jim's salary".to_string())).with_empty_span() + )), }), ast.selection, ); @@ -1275,9 +1514,9 @@ fn parse_escaped_single_quote_string_predicate_with_no_escape() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("salary"))), op: NotEq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "Jim''s salary".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("Jim''s salary".to_string())).with_empty_span() + )), }), ast.selection, ); @@ -1290,11 +1529,14 @@ fn parse_number() { #[cfg(feature = "bigdecimal")] assert_eq!( expr, - Expr::Value(Value::Number(bigdecimal::BigDecimal::from(1), false)) + Expr::Value((Value::Number(bigdecimal::BigDecimal::from(1), false)).with_empty_span()) ); #[cfg(not(feature = "bigdecimal"))] - assert_eq!(expr, Expr::Value(Value::Number("1.0".into(), false))); + assert_eq!( + expr, + Expr::Value((Value::Number("1.0".into(), false)).with_empty_span()) + ); } #[test] @@ -1446,15 +1688,15 @@ fn parse_json_object() { }) => assert_eq!( &[ FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), + name: Expr::Value((Value::SingleQuotedString("name".into())).with_empty_span()), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("value".into())).with_empty_span() + )), operator: FunctionArgOperator::Colon }, FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("type".into())), - arg: FunctionArgExpr::Expr(Expr::Value(number("1"))), + name: Expr::Value((Value::SingleQuotedString("type".into())).with_empty_span()), + arg: FunctionArgExpr::Expr(Expr::value(number("1"))), operator: FunctionArgOperator::Colon } ], @@ -1472,15 +1714,19 @@ fn parse_json_object() { assert_eq!( &[ FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), + name: Expr::Value( + (Value::SingleQuotedString("name".into())).with_empty_span() + ), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("value".into())).with_empty_span() + )), operator: FunctionArgOperator::Colon }, FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("type".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::Null)), + name: Expr::Value( + (Value::SingleQuotedString("type".into())).with_empty_span() + ), + arg: FunctionArgExpr::Expr(Expr::Value((Value::Null).with_empty_span())), operator: FunctionArgOperator::Colon } ], @@ -1537,10 +1783,10 @@ fn parse_json_object() { }) => { assert_eq!( &FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), + name: Expr::Value((Value::SingleQuotedString("name".into())).with_empty_span()), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("value".into())).with_empty_span() + )), operator: FunctionArgOperator::Colon }, &args[0] @@ -1548,7 +1794,10 @@ fn parse_json_object() { assert!(matches!( args[1], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::Function(_)), operator: FunctionArgOperator::Colon } @@ -1572,10 +1821,10 @@ fn parse_json_object() { }) => { assert_eq!( &FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), + name: Expr::Value((Value::SingleQuotedString("name".into())).with_empty_span()), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("value".into())).with_empty_span() + )), operator: FunctionArgOperator::Colon }, &args[0] @@ -1583,7 +1832,10 @@ fn parse_json_object() { assert!(matches!( args[1], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::Function(_)), operator: FunctionArgOperator::Colon } @@ -1692,9 +1944,9 @@ fn parse_not_precedence() { Expr::UnaryOp { op: UnaryOperator::Not, expr: Box::new(Expr::Between { - expr: Box::new(Expr::Value(number("1"))), - low: Box::new(Expr::Value(number("1"))), - high: Box::new(Expr::Value(number("2"))), + expr: Box::new(Expr::value(number("1"))), + low: Box::new(Expr::value(number("1"))), + high: Box::new(Expr::value(number("2"))), negated: true, }), }, @@ -1707,9 +1959,13 @@ fn parse_not_precedence() { Expr::UnaryOp { op: UnaryOperator::Not, expr: Box::new(Expr::Like { - expr: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("a".into())).with_empty_span() + )), negated: true, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("b".into())).with_empty_span() + )), escape_char: None, any: false, }), @@ -1724,7 +1980,9 @@ fn parse_not_precedence() { op: UnaryOperator::Not, expr: Box::new(Expr::InList { expr: Box::new(Expr::Identifier("a".into())), - list: vec![Expr::Value(Value::SingleQuotedString("a".into()))], + list: vec![Expr::Value( + (Value::SingleQuotedString("a".into())).with_empty_span() + )], negated: true, }), }, @@ -1744,7 +2002,7 @@ fn parse_null_like() { expr: Box::new(Expr::Identifier(Ident::new("column1"))), any: false, negated: false, - pattern: Box::new(Expr::Value(Value::Null)), + pattern: Box::new(Expr::Value((Value::Null).with_empty_span())), escape_char: None, }, alias: Ident { @@ -1758,7 +2016,7 @@ fn parse_null_like() { assert_eq!( SelectItem::ExprWithAlias { expr: Expr::Like { - expr: Box::new(Expr::Value(Value::Null)), + expr: Box::new(Expr::Value((Value::Null).with_empty_span())), any: false, negated: false, pattern: Box::new(Expr::Identifier(Ident::new("column1"))), @@ -1786,7 +2044,9 @@ fn parse_ilike() { Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, any: false, }, @@ -1803,8 +2063,10 @@ fn parse_ilike() { Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('^'.to_string()), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), any: false, }, select.selection.unwrap() @@ -1821,7 +2083,9 @@ fn parse_ilike() { Expr::IsNull(Box::new(Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, any: false, })), @@ -1844,7 +2108,9 @@ fn parse_like() { Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, any: false, }, @@ -1861,8 +2127,10 @@ fn parse_like() { Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('^'.to_string()), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), any: false, }, select.selection.unwrap() @@ -1879,7 +2147,9 @@ fn parse_like() { Expr::IsNull(Box::new(Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, any: false, })), @@ -1902,7 +2172,9 @@ fn parse_similar_to() { Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, }, select.selection.unwrap() @@ -1918,8 +2190,27 @@ fn parse_similar_to() { Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('^'.to_string()), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), + 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() ); @@ -1934,8 +2225,10 @@ fn parse_similar_to() { Expr::IsNull(Box::new(Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('^'.to_string()), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), })), select.selection.unwrap() ); @@ -1956,8 +2249,8 @@ fn parse_in_list() { Expr::InList { expr: Box::new(Expr::Identifier(Ident::new("segment"))), list: vec![ - Expr::Value(Value::SingleQuotedString("HIGH".to_string())), - Expr::Value(Value::SingleQuotedString("MED".to_string())), + Expr::Value((Value::SingleQuotedString("HIGH".to_string())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("MED".to_string())).with_empty_span()), ], negated, }, @@ -1982,6 +2275,22 @@ fn parse_in_subquery() { ); } +#[test] +fn parse_in_union() { + let sql = "SELECT * FROM customers WHERE segment IN ((SELECT segm FROM bar) UNION (SELECT segm FROM bar2))"; + let select = verified_only_select(sql); + assert_eq!( + Expr::InSubquery { + expr: Box::new(Expr::Identifier(Ident::new("segment"))), + subquery: Box::new(verified_query( + "(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)" + )), + negated: false, + }, + select.selection.unwrap() + ); +} + #[test] fn parse_in_unnest() { fn chk(negated: bool) { @@ -2099,8 +2408,8 @@ fn parse_between() { assert_eq!( Expr::Between { expr: Box::new(Expr::Identifier(Ident::new("age"))), - low: Box::new(Expr::Value(number("25"))), - high: Box::new(Expr::Value(number("32"))), + low: Box::new(Expr::value(number("25"))), + high: Box::new(Expr::value(number("32"))), negated, }, select.selection.unwrap() @@ -2117,16 +2426,16 @@ fn parse_between_with_expr() { let select = verified_only_select(sql); assert_eq!( Expr::IsNull(Box::new(Expr::Between { - expr: Box::new(Expr::Value(number("1"))), + expr: Box::new(Expr::value(number("1"))), low: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + left: Box::new(Expr::value(number("1"))), op: Plus, - right: Box::new(Expr::Value(number("2"))), + right: Box::new(Expr::value(number("2"))), }), high: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("3"))), + left: Box::new(Expr::value(number("3"))), op: Plus, - right: Box::new(Expr::Value(number("4"))), + right: Box::new(Expr::value(number("4"))), }), negated: false, })), @@ -2138,19 +2447,19 @@ fn parse_between_with_expr() { assert_eq!( Expr::BinaryOp { left: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + left: Box::new(Expr::value(number("1"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), op: BinaryOperator::And, right: Box::new(Expr::Between { expr: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + left: Box::new(Expr::value(number("1"))), op: BinaryOperator::Plus, right: Box::new(Expr::Identifier(Ident::new("x"))), }), - low: Box::new(Expr::Value(number("1"))), - high: Box::new(Expr::Value(number("2"))), + low: Box::new(Expr::value(number("1"))), + high: Box::new(Expr::value(number("2"))), negated: false, }), }, @@ -2165,13 +2474,15 @@ fn parse_tuples() { assert_eq!( vec![ SelectItem::UnnamedExpr(Expr::Tuple(vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), + Expr::value(number("1")), + Expr::value(number("2")), ])), - SelectItem::UnnamedExpr(Expr::Nested(Box::new(Expr::Value(number("1"))))), + SelectItem::UnnamedExpr(Expr::Nested(Box::new(Expr::Value( + (number("1")).with_empty_span() + )))), SelectItem::UnnamedExpr(Expr::Tuple(vec![ - Expr::Value(Value::SingleQuotedString("foo".into())), - Expr::Value(number("3")), + Expr::Value((Value::SingleQuotedString("foo".into())).with_empty_span()), + Expr::value(number("3")), Expr::Identifier(Ident::new("baz")), ])), ], @@ -2201,27 +2512,33 @@ fn parse_select_order_by() { fn chk(sql: &str) { let select = verified_query(sql); assert_eq!( - vec![ + OrderByKind::Expressions(vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), - asc: Some(true), - nulls_first: None, + options: OrderByOptions { + asc: Some(true), + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("id")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }, - ], - select.order_by.expect("ORDER BY expected").exprs + ]), + select.order_by.expect("ORDER BY expected").kind ); } chk("SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY lname ASC, fname DESC, id"); @@ -2236,48 +2553,198 @@ fn parse_select_order_by_limit() { ORDER BY lname ASC, fname DESC LIMIT 2"; let select = verified_query(sql); assert_eq!( - vec![ + OrderByKind::Expressions(vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), - asc: Some(true), - nulls_first: None, + options: OrderByOptions { + asc: Some(true), + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }, - ], - select.order_by.expect("ORDER BY expected").exprs + ]), + select.order_by.expect("ORDER BY expected").kind ); - assert_eq!(Some(Expr::Value(number("2"))), select.limit); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("2"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), select.limit_clause); } #[test] -fn parse_select_order_by_nulls_order() { - let sql = "SELECT id, fname, lname FROM customer WHERE id < 5 \ - ORDER BY lname ASC NULLS FIRST, fname DESC NULLS LAST LIMIT 2"; - let select = verified_query(sql); - assert_eq!( - vec![ - OrderByExpr { - expr: Expr::Identifier(Ident::new("lname")), +fn parse_select_order_by_all() { + fn chk(sql: &str, except_order_by: OrderByKind) { + let dialects = all_dialects_where(|d| d.supports_order_by_all()); + let select = dialects.verified_query(sql); + assert_eq!( + except_order_by, + select.order_by.expect("ORDER BY expected").kind + ); + } + let test_cases = [ + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL", + OrderByKind::All(OrderByOptions { + asc: None, + nulls_first: None, + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL NULLS FIRST", + OrderByKind::All(OrderByOptions { + asc: None, + nulls_first: Some(true), + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL NULLS LAST", + OrderByKind::All(OrderByOptions { + asc: None, + nulls_first: Some(false), + }), + ), + ( + "SELECT id, fname, lname FROM customer ORDER BY ALL ASC", + OrderByKind::All(OrderByOptions { + asc: Some(true), + nulls_first: None, + }), + ), + ( + "SELECT id, fname, lname FROM customer ORDER BY ALL ASC NULLS FIRST", + OrderByKind::All(OrderByOptions { asc: Some(true), nulls_first: Some(true), - with_fill: None, - }, - OrderByExpr { - expr: Expr::Identifier(Ident::new("fname")), + }), + ), + ( + "SELECT id, fname, lname FROM customer ORDER BY ALL ASC NULLS LAST", + OrderByKind::All(OrderByOptions { + asc: Some(true), + nulls_first: Some(false), + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC", + OrderByKind::All(OrderByOptions { + asc: Some(false), + nulls_first: None, + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC NULLS FIRST", + OrderByKind::All(OrderByOptions { + asc: Some(false), + nulls_first: Some(true), + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC NULLS LAST", + OrderByKind::All(OrderByOptions { asc: Some(false), nulls_first: Some(false), - with_fill: None, - }, - ], - select.order_by.expect("ORDER BY expeccted").exprs - ); - assert_eq!(Some(Expr::Value(number("2"))), select.limit); + }), + ), + ]; + + for (sql, expected_order_by) in test_cases { + chk(sql, expected_order_by); + } +} + +#[test] +fn parse_select_order_by_not_support_all() { + fn chk(sql: &str, except_order_by: OrderByKind) { + let dialects = all_dialects_where(|d| !d.supports_order_by_all()); + let select = dialects.verified_query(sql); + assert_eq!( + except_order_by, + select.order_by.expect("ORDER BY expected").kind + ); + } + let test_cases = [ + ( + "SELECT id, ALL FROM customer WHERE id < 5 ORDER BY ALL", + OrderByKind::Expressions(vec![OrderByExpr { + expr: Expr::Identifier(Ident::new("ALL")), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }]), + ), + ( + "SELECT id, ALL FROM customer ORDER BY ALL ASC NULLS FIRST", + OrderByKind::Expressions(vec![OrderByExpr { + expr: Expr::Identifier(Ident::new("ALL")), + options: OrderByOptions { + asc: Some(true), + nulls_first: Some(true), + }, + with_fill: None, + }]), + ), + ( + "SELECT id, ALL FROM customer ORDER BY ALL DESC NULLS LAST", + OrderByKind::Expressions(vec![OrderByExpr { + expr: Expr::Identifier(Ident::new("ALL")), + options: OrderByOptions { + asc: Some(false), + nulls_first: Some(false), + }, + with_fill: None, + }]), + ), + ]; + + for (sql, expected_order_by) in test_cases { + chk(sql, expected_order_by); + } +} + +#[test] +fn parse_select_order_by_nulls_order() { + let sql = "SELECT id, fname, lname FROM customer WHERE id < 5 \ + ORDER BY lname ASC NULLS FIRST, fname DESC NULLS LAST LIMIT 2"; + let select = verified_query(sql); + assert_eq!( + OrderByKind::Expressions(vec![ + OrderByExpr { + expr: Expr::Identifier(Ident::new("lname")), + options: OrderByOptions { + asc: Some(true), + nulls_first: Some(true), + }, + with_fill: None, + }, + OrderByExpr { + expr: Expr::Identifier(Ident::new("fname")), + options: OrderByOptions { + asc: Some(false), + nulls_first: Some(false), + }, + with_fill: None, + }, + ]), + select.order_by.expect("ORDER BY expeccted").kind + ); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::value(number("2"))), + offset: None, + limit_by: vec![], + }; + assert_eq!(Some(expected_limit_clause), select.limit_clause); } #[test] @@ -2314,6 +2781,124 @@ fn parse_select_group_by_all() { ); } +#[test] +fn parse_group_by_with_modifier() { + let clauses = ["x", "a, b", "ALL"]; + let modifiers = [ + "WITH ROLLUP", + "WITH CUBE", + "WITH TOTALS", + "WITH ROLLUP WITH CUBE", + ]; + let expected_modifiers = [ + vec![GroupByWithModifier::Rollup], + vec![GroupByWithModifier::Cube], + vec![GroupByWithModifier::Totals], + vec![GroupByWithModifier::Rollup, GroupByWithModifier::Cube], + ]; + let dialects = all_dialects_where(|d| d.supports_group_by_with_modifier()); + + for clause in &clauses { + for (modifier, expected_modifier) in modifiers.iter().zip(expected_modifiers.iter()) { + let sql = format!("SELECT * FROM t GROUP BY {clause} {modifier}"); + match dialects.verified_stmt(&sql) { + Statement::Query(query) => { + let group_by = &query.body.as_select().unwrap().group_by; + if clause == &"ALL" { + assert_eq!(group_by, &GroupByExpr::All(expected_modifier.to_vec())); + } else { + assert_eq!( + group_by, + &GroupByExpr::Expressions( + clause + .split(", ") + .map(|c| Identifier(Ident::new(c))) + .collect(), + expected_modifier.to_vec() + ) + ); + } + } + _ => unreachable!(), + } + } + } + + // invalid cases + let invalid_cases = [ + "SELECT * FROM t GROUP BY x WITH", + "SELECT * FROM t GROUP BY x WITH ROLLUP CUBE", + "SELECT * FROM t GROUP BY x WITH WITH ROLLUP", + "SELECT * FROM t GROUP BY WITH ROLLUP", + ]; + for sql in invalid_cases { + dialects + .parse_sql_statements(sql) + .expect_err("Expected: one of ROLLUP or CUBE or TOTALS, found: WITH"); + } +} + +#[test] +fn parse_group_by_special_grouping_sets() { + let sql = "SELECT a, b, SUM(c) FROM tab1 GROUP BY a, b GROUPING SETS ((a, b), (a), (b), ())"; + match all_dialects().verified_stmt(sql) { + 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![] + ]))] + ) + ); + } + _ => unreachable!(), + } +} + +#[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"; @@ -2321,7 +2906,7 @@ fn parse_select_having() { assert_eq!( Some(Expr::BinaryOp { left: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("COUNT")]), + name: ObjectName::from(vec![Ident::new("COUNT")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -2335,7 +2920,7 @@ fn parse_select_having() { within_group: vec![] })), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), select.having ); @@ -2352,7 +2937,7 @@ fn parse_select_qualify() { assert_eq!( Some(Expr::BinaryOp { left: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("ROW_NUMBER")]), + name: ObjectName::from(vec![Ident::new("ROW_NUMBER")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -2367,8 +2952,10 @@ fn parse_select_qualify() { partition_by: vec![Expr::Identifier(Ident::new("p"))], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }], window_frame: None, @@ -2376,7 +2963,7 @@ fn parse_select_qualify() { within_group: vec![] })), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), select.qualify ); @@ -2387,7 +2974,7 @@ fn parse_select_qualify() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("row_num"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), select.qualify ); @@ -2399,6 +2986,14 @@ fn parse_limit_accepts_all() { "SELECT id, fname, lname FROM customer WHERE id = 1 LIMIT ALL", "SELECT id, fname, lname FROM customer WHERE id = 1", ); + one_statement_parses_to( + "SELECT id, fname, lname FROM customer WHERE id = 1 LIMIT ALL OFFSET 1", + "SELECT id, fname, lname FROM customer WHERE id = 1 OFFSET 1", + ); + one_statement_parses_to( + "SELECT id, fname, lname FROM customer WHERE id = 1 OFFSET 1 LIMIT ALL", + "SELECT id, fname, lname FROM customer WHERE id = 1 OFFSET 1", + ); } #[test] @@ -2500,7 +3095,7 @@ fn parse_cast() { &Expr::Cast { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Varbinary(Some(50)), + data_type: DataType::Varbinary(Some(BinaryLength::IntegerLength { length: 50 })), format: None, }, expr_from_projection(only(&select.projection)) @@ -2759,7 +3354,7 @@ fn parse_listagg() { assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("LISTAGG")]), + name: ObjectName::from(vec![Ident::new("LISTAGG")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -2769,14 +3364,14 @@ fn parse_listagg() { "dateid" )))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(", ".to_owned()) + (Value::SingleQuotedString(", ".to_owned())).with_empty_span() ))) ], clauses: vec![FunctionArgumentClause::OnOverflow( ListAggOnOverflow::Truncate { - filler: Some(Box::new(Expr::Value(Value::SingleQuotedString( - "%".to_string(), - )))), + filler: Some(Box::new(Expr::Value( + (Value::SingleQuotedString("%".to_string(),)).with_empty_span() + ))), with_count: false, } )], @@ -2791,8 +3386,10 @@ fn parse_listagg() { quote_style: None, span: Span::empty(), }), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }, OrderByExpr { @@ -2801,8 +3398,10 @@ fn parse_listagg() { quote_style: None, span: Span::empty(), }), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }, ] @@ -2914,7 +3513,10 @@ fn parse_window_function_null_treatment_arg() { let SelectItem::UnnamedExpr(Expr::Function(actual)) = &projection[i] else { unreachable!() }; - assert_eq!(ObjectName(vec![Ident::new("FIRST_VALUE")]), actual.name); + assert_eq!( + ObjectName::from(vec![Ident::new("FIRST_VALUE")]), + actual.name + ); let FunctionArguments::List(arg_list) = &actual.args else { panic!("expected argument list") }; @@ -3004,7 +3606,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]); @@ -3025,13 +3627,13 @@ fn gen_number_case(value: &str) -> (Vec, Vec) { format!("{} AS col_alias", value), ]; let expected = vec![ - SelectItem::UnnamedExpr(Expr::Value(number(value))), + SelectItem::UnnamedExpr(Expr::value(number(value))), SelectItem::ExprWithAlias { - expr: Expr::Value(number(value)), + expr: Expr::value(number(value)), alias: Ident::new("col_alias"), }, SelectItem::ExprWithAlias { - expr: Expr::Value(number(value)), + expr: Expr::value(number(value)), alias: Ident::new("col_alias"), }, ]; @@ -3052,19 +3654,19 @@ fn gen_sign_number_case(value: &str, op: UnaryOperator) -> (Vec, Vec 0")), + option: ColumnOption::Check(CheckConstraint { + name: None, + expr: Box::new(verified_expr("constrained > 0")), + enforced: None, + }), }, ], }, ColumnDef { name: "ref".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::ForeignKey { - foreign_table: ObjectName(vec!["othertable".into()]), + option: ColumnOption::ForeignKey(ForeignKeyConstraint { + name: None, + index_name: None, + columns: vec![], + foreign_table: ObjectName::from(vec!["othertable".into()]), referred_columns: vec!["a".into(), "b".into()], on_delete: None, on_update: None, + match_kind: None, characteristics: None, - }, + }), }], }, ColumnDef { name: "ref2".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::ForeignKey { - foreign_table: ObjectName(vec!["othertable2".into()]), + option: ColumnOption::ForeignKey(ForeignKeyConstraint { + name: None, + index_name: None, + columns: vec![], + foreign_table: ObjectName::from(vec!["othertable2".into()]), referred_columns: vec![], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::NoAction), + match_kind: None, characteristics: None, - }, + }), },], }, ] @@ -3238,45 +3856,57 @@ fn parse_create_table() { assert_eq!( constraints, vec![ - TableConstraint::ForeignKey { + ForeignKeyConstraint { name: Some("fkey".into()), + index_name: None, columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable3".into()]), + foreign_table: ObjectName::from(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), on_update: None, + match_kind: None, characteristics: None, - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: Some("fkey2".into()), + index_name: None, columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), on_update: Some(ReferentialAction::Restrict), + match_kind: None, characteristics: None, - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: None, + index_name: None, columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::SetDefault), + match_kind: None, characteristics: None, - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: None, + index_name: None, columns: vec!["lng".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], on_delete: None, on_update: Some(ReferentialAction::SetNull), + match_kind: None, characteristics: None, - }, + } + .into(), ] ); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); } _ => unreachable!(), } @@ -3321,7 +3951,7 @@ fn parse_create_table_with_constraint_characteristics() { name, columns, constraints, - with_options, + table_options, if_not_exists: false, external: false, file_format: None, @@ -3338,7 +3968,6 @@ fn parse_create_table_with_constraint_characteristics() { length: 100, unit: None, })), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -3347,7 +3976,6 @@ fn parse_create_table_with_constraint_characteristics() { ColumnDef { name: "lat".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Null, @@ -3356,7 +3984,6 @@ fn parse_create_table_with_constraint_characteristics() { ColumnDef { name: "lng".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![], }, ] @@ -3364,61 +3991,73 @@ fn parse_create_table_with_constraint_characteristics() { assert_eq!( constraints, vec![ - TableConstraint::ForeignKey { + ForeignKeyConstraint { name: Some("fkey".into()), + index_name: None, columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable3".into()]), + foreign_table: ObjectName::from(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), on_update: None, + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Deferred), enforced: None }), - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: Some("fkey2".into()), + index_name: None, columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), on_update: Some(ReferentialAction::Restrict), + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Immediate), enforced: None, }), - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: None, + index_name: None, columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::SetDefault), + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(false), initially: Some(DeferrableInitial::Deferred), enforced: Some(false), }), - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: None, + index_name: None, columns: vec!["lng".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], on_delete: None, on_update: Some(ReferentialAction::SetNull), + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(false), initially: Some(DeferrableInitial::Immediate), enforced: Some(true), }), - }, + } + .into(), ] ); - assert_eq!(with_options, vec![]); + assert_eq!(table_options, CreateTableOptions::None); } _ => unreachable!(), } @@ -3465,13 +4104,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() { @@ -3491,13 +4130,18 @@ fn parse_create_table_column_constraint_characteristics() { vec![ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: false, - characteristics: expected_value - } + option: ColumnOption::Unique(UniqueConstraint { + name: None, + index_name: None, + index_type_display: KeyOrIndexDisplay::None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: expected_value, + nulls_distinct: NullsDistinctOption::None, + }) }] }], "{message}" @@ -3599,20 +4243,18 @@ fn parse_create_table_hive_array() { .. }) => { assert!(if_not_exists); - assert_eq!(name, ObjectName(vec!["something".into()])); + assert_eq!(name, ObjectName::from(vec!["something".into()])); assert_eq!( columns, vec![ ColumnDef { name: Ident::new("name"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("val"), data_type: DataType::Array(expected), - collation: None, options: vec![], }, ], @@ -3689,7 +4331,10 @@ fn parse_assert_message() { message: Some(message), } => { match message { - Expr::Value(Value::SingleQuotedString(s)) => assert_eq!(s, "No rows in my_table"), + Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(s), + span: _, + }) => assert_eq!(s, "No rows in my_table"), _ => unreachable!(), }; } @@ -3707,6 +4352,15 @@ fn parse_create_schema() { } _ => unreachable!(), } + + verified_stmt(r#"CREATE SCHEMA a.b.c OPTIONS(key1 = 'value1', key2 = 'value2')"#); + 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] @@ -3758,8 +4412,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()); @@ -3784,19 +4439,18 @@ fn parse_create_table_as_table() { schema_name: None, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }); match verified_stmt(sql1) { Statement::CreateTable(CreateTable { query, name, .. }) => { - assert_eq!(name, ObjectName(vec![Ident::new("new_table")])); + assert_eq!(name, ObjectName::from(vec![Ident::new("new_table")])); assert_eq!(query.unwrap(), expected_query1); } _ => unreachable!(), @@ -3811,19 +4465,18 @@ fn parse_create_table_as_table() { schema_name: Some("schema_name".to_string()), }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }); match verified_stmt(sql2) { Statement::CreateTable(CreateTable { query, name, .. }) => { - assert_eq!(name, ObjectName(vec![Ident::new("new_table")])); + assert_eq!(name, ObjectName::from(vec![Ident::new("new_table")])); assert_eq!(query.unwrap(), expected_query2); } _ => unreachable!(), @@ -3866,20 +4519,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] @@ -3902,16 +4541,22 @@ 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 { key: "foo".into(), - value: Expr::Value(Value::SingleQuotedString("bar".into())), + value: Expr::Value( + (Value::SingleQuotedString("bar".into())).with_empty_span() + ), }, SqlOption::KeyValue { key: "a".into(), - value: Expr::Value(number("123")), + value: Expr::value(number("123")), }, ], with_options @@ -3926,8 +4571,8 @@ fn parse_create_table_clone() { let sql = "CREATE OR REPLACE TABLE a CLONE a_tmp"; match verified_stmt(sql) { Statement::CreateTable(CreateTable { name, clone, .. }) => { - assert_eq!(ObjectName(vec![Ident::new("a")]), name); - assert_eq!(Some(ObjectName(vec![(Ident::new("a_tmp"))])), clone) + assert_eq!(ObjectName::from(vec![Ident::new("a")]), name); + assert_eq!(Some(ObjectName::from(vec![(Ident::new("a_tmp"))])), clone) } _ => unreachable!(), } @@ -3961,7 +4606,7 @@ fn parse_create_external_table() { name, columns, constraints, - with_options, + table_options, if_not_exists, external, file_format, @@ -3978,7 +4623,6 @@ fn parse_create_external_table() { length: 100, unit: None, })), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -3987,7 +4631,6 @@ fn parse_create_external_table() { ColumnDef { name: "lat".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Null, @@ -3996,7 +4639,6 @@ fn parse_create_external_table() { ColumnDef { name: "lng".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![], }, ] @@ -4007,7 +4649,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!(), @@ -4032,7 +4674,7 @@ fn parse_create_or_replace_external_table() { name, columns, constraints, - with_options, + table_options, if_not_exists, external, file_format, @@ -4049,7 +4691,6 @@ fn parse_create_or_replace_external_table() { length: 100, unit: None, })), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -4062,7 +4703,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); } @@ -4113,7 +4754,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!(), }; @@ -4141,12 +4796,42 @@ fn parse_alter_table() { quote_style: Some('\''), span: Span::empty(), }, - value: Expr::Value(Value::SingleQuotedString("parquet".to_string())), + value: Expr::Value( + (Value::SingleQuotedString("parquet".to_string())).with_empty_span() + ), }], ); } _ => 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] @@ -4155,11 +4840,11 @@ fn parse_rename_table() { Statement::RenameTable(rename_tables) => { assert_eq!( vec![RenameTable { - old_name: ObjectName(vec![ + old_name: ObjectName::from(vec![ Ident::new("test".to_string()), Ident::new("test1".to_string()), ]), - new_name: ObjectName(vec![ + new_name: ObjectName::from(vec![ Ident::new("test_db".to_string()), Ident::new("test2".to_string()), ]), @@ -4177,16 +4862,16 @@ fn parse_rename_table() { assert_eq!( vec![ RenameTable { - old_name: ObjectName(vec![Ident::new("old_table1".to_string())]), - new_name: ObjectName(vec![Ident::new("new_table1".to_string())]), + old_name: ObjectName::from(vec![Ident::new("old_table1".to_string())]), + new_name: ObjectName::from(vec![Ident::new("new_table1".to_string())]), }, RenameTable { - old_name: ObjectName(vec![Ident::new("old_table2".to_string())]), - new_name: ObjectName(vec![Ident::new("new_table2".to_string())]), + old_name: ObjectName::from(vec![Ident::new("old_table2".to_string())]), + new_name: ObjectName::from(vec![Ident::new("new_table2".to_string())]), }, RenameTable { - old_name: ObjectName(vec![Ident::new("old_table3".to_string())]), - new_name: ObjectName(vec![Ident::new("new_table3".to_string())]), + old_name: ObjectName::from(vec![Ident::new("old_table3".to_string())]), + new_name: ObjectName::from(vec![Ident::new("new_table3".to_string())]), } ], rename_tables @@ -4213,9 +4898,9 @@ fn test_alter_table_with_on_cluster() { match all_dialects() .verified_stmt("ALTER TABLE t ON CLUSTER 'cluster' ADD CONSTRAINT bar PRIMARY KEY (baz)") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, on_cluster, .. - } => { + }) => { assert_eq!(name.to_string(), "t"); assert_eq!(on_cluster, Some(Ident::with_quote('\'', "cluster"))); } @@ -4225,9 +4910,9 @@ fn test_alter_table_with_on_cluster() { match all_dialects() .verified_stmt("ALTER TABLE t ON CLUSTER cluster_name ADD CONSTRAINT bar PRIMARY KEY (baz)") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, on_cluster, .. - } => { + }) => { assert_eq!(name.to_string(), "t"); assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); } @@ -4285,11 +4970,13 @@ fn parse_alter_view_with_options() { vec![ SqlOption::KeyValue { key: "foo".into(), - value: Expr::Value(Value::SingleQuotedString("bar".into())), + value: Expr::Value( + (Value::SingleQuotedString("bar".into())).with_empty_span() + ), }, SqlOption::KeyValue { key: "a".into(), - value: Expr::Value(number("123")), + value: Expr::value(number("123")), }, ], with_options @@ -4386,7 +5073,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!(), @@ -4401,22 +5088,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")), @@ -4455,7 +5146,7 @@ fn parse_alter_table_alter_column() { assert_eq!( op, AlterColumnOperation::SetDefault { - value: Expr::Value(test_utils::number("0")) + value: Expr::Value((test_utils::number("0")).with_empty_span()) } ); } @@ -4486,22 +5177,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!( @@ -4578,8 +5268,8 @@ fn run_explain_analyze( query: &str, expected_verbose: bool, expected_analyze: bool, - expected_format: Option, - exepcted_options: Option>, + expected_format: Option, + expected_options: Option>, ) { match dialect.verified_stmt(query) { Statement::Explain { @@ -4595,7 +5285,7 @@ fn run_explain_analyze( assert_eq!(verbose, expected_verbose); assert_eq!(analyze, expected_analyze); assert_eq!(format, expected_format); - assert_eq!(options, exepcted_options); + assert_eq!(options, expected_options); assert!(!query_plan); assert!(!estimate); assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); @@ -4689,7 +5379,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, ); @@ -4698,7 +5388,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, ); @@ -4707,7 +5406,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, ); } @@ -4781,7 +5480,7 @@ fn parse_named_argument_function() { assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("FUN")]), + name: ObjectName::from(vec![Ident::new("FUN")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -4789,16 +5488,16 @@ fn parse_named_argument_function() { args: vec![ FunctionArg::Named { name: Ident::new("a"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "1".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("1".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::RightArrow }, FunctionArg::Named { name: Ident::new("b"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "2".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("2".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::RightArrow }, ], @@ -4821,7 +5520,7 @@ fn parse_named_argument_function_with_eq_operator() { .verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("FUN")]), + name: ObjectName::from(vec![Ident::new("FUN")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -4829,16 +5528,16 @@ fn parse_named_argument_function_with_eq_operator() { args: vec![ FunctionArg::Named { name: Ident::new("a"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "1".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("1".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::Equals }, FunctionArg::Named { name: Ident::new("b"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "2".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("2".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::Equals }, ], @@ -4862,7 +5561,7 @@ fn parse_named_argument_function_with_eq_operator() { [Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("bar"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("42"))), + right: Box::new(Expr::value(number("42"))), }] ), ); @@ -4896,7 +5595,7 @@ fn parse_window_functions() { assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("row_number")]), + name: ObjectName::from(vec![Ident::new("row_number")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -4911,8 +5610,10 @@ fn parse_window_functions() { partition_by: vec![], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("dt")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }], window_frame: None, @@ -4952,7 +5653,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()); @@ -4982,6 +5684,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 \ @@ -4994,10 +5697,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()), @@ -5007,6 +5714,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 \ @@ -5014,7 +5722,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, @@ -5023,7 +5731,7 @@ fn test_parse_named_window() { projection: vec![ SelectItem::ExprWithAlias { expr: Expr::Function(Function { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "MIN".to_string(), quote_style: None, span: Span::empty(), @@ -5058,7 +5766,7 @@ fn test_parse_named_window() { }, SelectItem::ExprWithAlias { expr: Expr::Function(Function { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "MAX".to_string(), quote_style: None, span: Span::empty(), @@ -5092,9 +5800,10 @@ fn test_parse_named_window() { }, }, ], + exclude: None, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "aggregate_test_100".to_string(), quote_style: None, span: Span::empty(), @@ -5125,8 +5834,10 @@ fn test_parse_named_window() { quote_style: None, span: Span::empty(), }), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }], window_frame: None, @@ -5154,12 +5865,17 @@ fn test_parse_named_window() { window_before_qualify: true, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }; assert_eq!(actual_select_only, expected); } #[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 \ @@ -5167,7 +5883,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 \ @@ -5176,7 +5892,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] @@ -5207,14 +5923,14 @@ fn parse_literal_integer() { let select = verified_only_select(sql); assert_eq!(3, select.projection.len()); assert_eq!( - &Expr::Value(number("1")), + &Expr::value(number("1")), expr_from_projection(&select.projection[0]), ); // negative literal is parsed as a - and expr assert_eq!( &UnaryOp { op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(number("10"))) + expr: Box::new(Expr::value(number("10"))) }, expr_from_projection(&select.projection[1]), ); @@ -5222,7 +5938,7 @@ fn parse_literal_integer() { assert_eq!( &UnaryOp { op: UnaryOperator::Plus, - expr: Box::new(Expr::Value(number("20"))) + expr: Box::new(Expr::value(number("20"))) }, expr_from_projection(&select.projection[2]), ) @@ -5236,11 +5952,11 @@ fn parse_literal_decimal() { let select = verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( - &Expr::Value(number("0.300000000000000004")), + &Expr::value(number("0.300000000000000004")), expr_from_projection(&select.projection[0]), ); assert_eq!( - &Expr::Value(number("9007199254740993.0")), + &Expr::value(number("9007199254740993.0")), expr_from_projection(&select.projection[1]), ) } @@ -5251,15 +5967,17 @@ fn parse_literal_string() { let select = verified_only_select(sql); assert_eq!(3, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedString("one".to_string())), + &Expr::Value((Value::SingleQuotedString("one".to_string())).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::NationalStringLiteral("national string".to_string())), + &Expr::Value( + (Value::NationalStringLiteral("national string".to_string())).with_empty_span() + ), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::HexStringLiteral("deadBEEF".to_string())), + &Expr::Value((Value::HexStringLiteral("deadBEEF".to_string())).with_empty_span()), expr_from_projection(&select.projection[2]) ); @@ -5272,10 +5990,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: "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)), ); } @@ -5285,10 +6007,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: "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)), ); } @@ -5298,10 +6024,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: "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)), ); } @@ -5311,10 +6041,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: "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)), ); @@ -5326,10 +6060,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: "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)), ); @@ -5344,7 +6082,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("1-1")))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1-1"))).with_empty_span() + )), leading_field: Some(DateTimeField::Year), leading_precision: None, last_field: Some(DateTimeField::Month), @@ -5357,9 +6097,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "01:01.01" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("01:01.01"))).with_empty_span() + )), leading_field: Some(DateTimeField::Minute), leading_precision: Some(5), last_field: Some(DateTimeField::Second), @@ -5372,7 +6112,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("1")))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1"))).with_empty_span() + )), leading_field: Some(DateTimeField::Second), leading_precision: Some(5), last_field: None, @@ -5385,7 +6127,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("10")))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("10"))).with_empty_span() + )), leading_field: Some(DateTimeField::Hour), leading_precision: None, last_field: None, @@ -5398,7 +6142,7 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(number("5"))), + value: Box::new(Expr::value(number("5"))), leading_field: Some(DateTimeField::Day), leading_precision: None, last_field: None, @@ -5411,7 +6155,7 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(number("5"))), + value: Box::new(Expr::value(number("5"))), leading_field: Some(DateTimeField::Days), leading_precision: None, last_field: None, @@ -5424,7 +6168,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("10")))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("10"))).with_empty_span() + )), leading_field: Some(DateTimeField::Hour), leading_precision: Some(1), last_field: None, @@ -5493,9 +6239,9 @@ fn parse_interval_dont_require_unit() { let select = dialects.verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "1 DAY" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1 DAY"))).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -5532,9 +6278,9 @@ fn parse_interval_require_qualifier() { expr_from_projection(only(&select.projection)), &Expr::Interval(Interval { value: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + left: Box::new(Expr::value(number("1"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), leading_field: Some(DateTimeField::Day), leading_precision: None, @@ -5549,9 +6295,13 @@ fn parse_interval_require_qualifier() { expr_from_projection(only(&select.projection)), &Expr::Interval(Interval { value: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + left: Box::new(Expr::Value( + (Value::SingleQuotedString("1".to_string())).with_empty_span() + )), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("1".to_string())).with_empty_span() + )), }), leading_field: Some(DateTimeField::Day), leading_precision: None, @@ -5567,12 +6317,18 @@ fn parse_interval_require_qualifier() { &Expr::Interval(Interval { value: Box::new(Expr::BinaryOp { left: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + left: Box::new(Expr::Value( + (Value::SingleQuotedString("1".to_string())).with_empty_span() + )), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(Value::SingleQuotedString("2".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("2".to_string())).with_empty_span() + )), }), op: BinaryOperator::Minus, - right: Box::new(Expr::Value(Value::SingleQuotedString("3".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("3".to_string())).with_empty_span() + )), }), leading_field: Some(DateTimeField::Day), leading_precision: None, @@ -5591,9 +6347,9 @@ fn parse_interval_disallow_interval_expr() { assert_eq!( expr_from_projection(only(&select.projection)), &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "1 DAY" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1 DAY"))).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -5614,9 +6370,9 @@ fn parse_interval_disallow_interval_expr() { expr_from_projection(only(&select.projection)), &Expr::BinaryOp { left: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "1 DAY" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1 DAY"))).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -5624,9 +6380,9 @@ fn parse_interval_disallow_interval_expr() { })), op: BinaryOperator::Gt, right: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "1 SECOND" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1 SECOND"))).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -5644,9 +6400,9 @@ fn interval_disallow_interval_expr_gt() { expr, Expr::BinaryOp { left: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "1 second".to_string() - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("1 second".to_string())).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -5671,9 +6427,9 @@ fn interval_disallow_interval_expr_double_colon() { Expr::Cast { kind: CastKind::DoubleColon, expr: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "1 second".to_string() - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("1 second".to_string())).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -5706,9 +6462,10 @@ fn parse_interval_and_or_xor() { quote_style: None, span: Span::empty(), }))], + exclude: None, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "test".to_string(), quote_style: None, span: Span::empty(), @@ -5733,9 +6490,9 @@ fn parse_interval_and_or_xor() { })), op: BinaryOperator::Plus, right: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "5 days".to_string(), - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("5 days".to_string())).with_empty_span(), + )), leading_field: None, leading_precision: None, last_field: None, @@ -5759,9 +6516,9 @@ fn parse_interval_and_or_xor() { })), op: BinaryOperator::Plus, right: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "3 days".to_string(), - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("3 days".to_string())).with_empty_span(), + )), leading_field: None, leading_precision: None, last_field: None, @@ -5780,16 +6537,16 @@ fn parse_interval_and_or_xor() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }))]; assert_eq!(actual_ast, expected_ast); @@ -5815,15 +6572,15 @@ fn parse_interval_and_or_xor() { #[test] fn parse_at_timezone() { - let zero = Expr::Value(number("0")); + let zero = Expr::value(number("0")); let sql = "SELECT FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00' FROM t"; let select = verified_only_select(sql); assert_eq!( &Expr::AtTimeZone { timestamp: Box::new(call("FROM_UNIXTIME", [zero.clone()])), - time_zone: Box::new(Expr::Value(Value::SingleQuotedString( - "UTC-06:00".to_string() - ))), + time_zone: Box::new(Expr::Value( + (Value::SingleQuotedString("UTC-06:00".to_string())).with_empty_span() + )), }, expr_from_projection(only(&select.projection)), ); @@ -5837,11 +6594,13 @@ fn parse_at_timezone() { [ Expr::AtTimeZone { timestamp: Box::new(call("FROM_UNIXTIME", [zero])), - time_zone: Box::new(Expr::Value(Value::SingleQuotedString( - "UTC-06:00".to_string() - ))), + time_zone: Box::new(Expr::Value( + (Value::SingleQuotedString("UTC-06:00".to_string())).with_empty_span() + )), }, - Expr::Value(Value::SingleQuotedString("%Y-%m-%dT%H".to_string()),) + Expr::Value( + (Value::SingleQuotedString("%Y-%m-%dT%H".to_string())).with_empty_span() + ) ] ), alias: Ident { @@ -5878,9 +6637,11 @@ fn parse_json_keyword() { }'"#; let select = verified_only_select(sql); assert_eq!( - &Expr::TypedString { + &Expr::TypedString(TypedString { data_type: DataType::JSON, - value: r#"{ + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{ "id": 10, "type": "fruit", "name": "apple", @@ -5900,21 +6661,55 @@ fn parse_json_keyword() { ] } }"# - .into() - }, + .to_string() + ), + span: Span::empty(), + }, + uses_odbc_syntax: false, + }), expr_from_projection(only(&select.projection)), ); } +#[test] +fn parse_typed_strings() { + let expr = verified_expr(r#"JSON '{"foo":"bar"}'"#); + assert_eq!( + Expr::TypedString(TypedString { + data_type: DataType::JSON, + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }), + 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()); + } +} + #[test] 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: 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'"); @@ -5922,10 +6717,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: 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'"); @@ -5933,10 +6732,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: 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'"); @@ -5944,10 +6747,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: 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'"); @@ -5955,10 +6762,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: 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'"); @@ -5966,10 +6777,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: 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'"); @@ -5996,7 +6811,9 @@ fn parse_table_function() { assert_eq!( call( "FUN", - [Expr::Value(Value::SingleQuotedString("1".to_owned()))], + [Expr::Value( + (Value::SingleQuotedString("1".to_owned())).with_empty_span() + )], ), expr ); @@ -6179,9 +6996,9 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![call( "make_array", [ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ], )], with_offset: false, @@ -6205,14 +7022,14 @@ fn parse_unnest_in_from_clause() { call( "make_array", [ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ], ), call( "make_array", - [Expr::Value(number("5")), Expr::Value(number("6"))], + [Expr::value(number("5")), Expr::value(number("6"))], ), ], with_offset: false, @@ -6255,26 +7072,32 @@ 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![ - IsNull(Box::new(Identifier(Ident::new("bar")))), - BinaryOp { - left: Box::new(Identifier(Ident::new("bar"))), - op: Eq, - right: Box::new(Expr::Value(number("0"))), + CaseWhen { + condition: IsNull(Box::new(Identifier(Ident::new("bar")))), + result: Expr::value(Value::SingleQuotedString("null".to_string())), }, - BinaryOp { - left: Box::new(Identifier(Ident::new("bar"))), - op: GtEq, - right: Box::new(Expr::Value(number("0"))), + CaseWhen { + condition: BinaryOp { + left: Box::new(Identifier(Ident::new("bar"))), + op: Eq, + right: Box::new(Expr::value(number("0"))), + }, + result: Expr::value(Value::SingleQuotedString("=0".to_string())), + }, + CaseWhen { + condition: BinaryOp { + left: Box::new(Identifier(Ident::new("bar"))), + op: GtEq, + right: Box::new(Expr::value(number("0"))), + }, + result: Expr::value(Value::SingleQuotedString(">=0".to_string())), }, ], - results: vec![ - Expr::Value(Value::SingleQuotedString("null".to_string())), - Expr::Value(Value::SingleQuotedString("=0".to_string())), - Expr::Value(Value::SingleQuotedString(">=0".to_string())), - ], - else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( + else_result: Some(Box::new(Expr::value(Value::SingleQuotedString( "<0".to_string() )))), }, @@ -6290,10 +7113,14 @@ 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![Expr::Value(number("1"))], - results: vec![Expr::Value(Value::SingleQuotedString("Y".to_string()))], - else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( + conditions: vec![CaseWhen { + condition: Expr::value(number("1")), + result: Expr::value(Value::SingleQuotedString("Y".to_string())), + }], + else_result: Some(Box::new(Expr::value(Value::SingleQuotedString( "N".to_string() )))), }, @@ -6320,11 +7147,11 @@ fn parse_implicit_join() { assert_eq!( vec![ TableWithJoins { - relation: table_from_name(ObjectName(vec!["t1".into()])), + relation: table_from_name(ObjectName::from(vec!["t1".into()])), joins: vec![], }, TableWithJoins { - relation: table_from_name(ObjectName(vec!["t2".into()])), + relation: table_from_name(ObjectName::from(vec!["t2".into()])), joins: vec![], }, ], @@ -6336,19 +7163,19 @@ fn parse_implicit_join() { assert_eq!( vec![ TableWithJoins { - relation: table_from_name(ObjectName(vec!["t1a".into()])), + relation: table_from_name(ObjectName::from(vec!["t1a".into()])), joins: vec![Join { - relation: table_from_name(ObjectName(vec!["t1b".into()])), + relation: table_from_name(ObjectName::from(vec!["t1b".into()])), global: false, - join_operator: JoinOperator::Inner(JoinConstraint::Natural), + join_operator: JoinOperator::Join(JoinConstraint::Natural), }], }, TableWithJoins { - relation: table_from_name(ObjectName(vec!["t2a".into()])), + relation: table_from_name(ObjectName::from(vec!["t2a".into()])), joins: vec![Join { - relation: table_from_name(ObjectName(vec!["t2b".into()])), + relation: table_from_name(ObjectName::from(vec!["t2b".into()])), global: false, - join_operator: JoinOperator::Inner(JoinConstraint::Natural), + join_operator: JoinOperator::Join(JoinConstraint::Natural), }], }, ], @@ -6362,14 +7189,47 @@ fn parse_cross_join() { let select = verified_only_select(sql); assert_eq!( Join { - relation: table_from_name(ObjectName(vec![Ident::new("t2")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("t2")])), global: false, - join_operator: JoinOperator::CrossJoin, + join_operator: JoinOperator::CrossJoin(JoinConstraint::None), }, only(only(select.from).joins), ); } +#[test] +fn parse_cross_join_constraint() { + fn join_with_constraint(constraint: JoinConstraint) -> Join { + Join { + relation: table_from_name(ObjectName::from(vec![Ident::new("t2")])), + global: false, + join_operator: JoinOperator::CrossJoin(constraint), + } + } + + fn test_constraint(sql: &str, constraint: JoinConstraint) { + let dialect = all_dialects_where(|d| d.supports_cross_join_constraint()); + let select = dialect.verified_only_select(sql); + assert_eq!( + join_with_constraint(constraint), + only(only(select.from).joins), + ); + } + + test_constraint( + "SELECT * FROM t1 CROSS JOIN t2 ON a = b", + JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }), + ); + test_constraint( + "SELECT * FROM t1 CROSS JOIN t2 USING(a)", + JoinConstraint::Using(vec![ObjectName::from(vec![Ident::new("a")])]), + ); +} + #[test] fn parse_joins_on() { fn join_with_constraint( @@ -6380,7 +7240,7 @@ fn parse_joins_on() { ) -> Join { Join { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new(relation.into())]), + name: ObjectName::from(vec![Ident::new(relation.into())]), alias, args: None, with_hints: vec![], @@ -6389,6 +7249,7 @@ fn parse_joins_on() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, global, join_operator: f(JoinConstraint::On(Expr::BinaryOp { @@ -6405,7 +7266,7 @@ fn parse_joins_on() { "t2", table_alias("foo"), false, - JoinOperator::Inner, + JoinOperator::Join, )] ); one_statement_parses_to( @@ -6415,10 +7276,18 @@ fn parse_joins_on() { // Test parsing of different join operators assert_eq!( only(&verified_only_select("SELECT * FROM t1 JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, false, JoinOperator::Join)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 INNER JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint("t2", None, false, JoinOperator::Inner)] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, false, JoinOperator::Left)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 LEFT OUTER JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint( "t2", None, @@ -6428,6 +7297,10 @@ fn parse_joins_on() { ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 RIGHT JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, false, JoinOperator::Right)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 RIGHT OUTER JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint( "t2", None, @@ -6509,7 +7382,7 @@ fn parse_joins_using() { ) -> Join { Join { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new(relation.into())]), + name: ObjectName::from(vec![Ident::new(relation.into())]), alias, args: None, with_hints: vec![], @@ -6518,9 +7391,12 @@ fn parse_joins_using() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, global: false, - join_operator: f(JoinConstraint::Using(vec!["c1".into()])), + join_operator: f(JoinConstraint::Using(vec![ObjectName::from(vec![ + "c1".into() + ])])), } } // Test parsing of aliases @@ -6529,7 +7405,7 @@ fn parse_joins_using() { vec![join_with_constraint( "t2", table_alias("foo"), - JoinOperator::Inner, + JoinOperator::Join, )] ); one_statement_parses_to( @@ -6539,14 +7415,26 @@ fn parse_joins_using() { // Test parsing of different join operators assert_eq!( only(&verified_only_select("SELECT * FROM t1 JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::Join)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 INNER JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::Inner)] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::Left)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 LEFT OUTER JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::LeftOuter)] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 RIGHT JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::Right)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 RIGHT OUTER JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::RightOuter)] ); assert_eq!( @@ -6577,6 +7465,7 @@ fn parse_joins_using() { only(&verified_only_select("SELECT * FROM t1 FULL JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::FullOuter)] ); + verified_stmt("SELECT * FROM tbl1 AS t1 JOIN tbl2 AS t2 USING(t2.col1)"); } #[test] @@ -6584,7 +7473,7 @@ fn parse_natural_join() { fn natural_join(f: impl Fn(JoinConstraint) -> JoinOperator, alias: Option) -> Join { Join { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t2")]), + name: ObjectName::from(vec![Ident::new("t2")]), alias, args: None, with_hints: vec![], @@ -6593,26 +7482,46 @@ fn parse_natural_join() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, global: false, join_operator: f(JoinConstraint::Natural), } } - // if not specified, inner join as default + // unspecified join assert_eq!( only(&verified_only_select("SELECT * FROM t1 NATURAL JOIN t2").from).joins, + vec![natural_join(JoinOperator::Join, None)] + ); + + // inner join explicitly + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 NATURAL INNER JOIN t2").from).joins, vec![natural_join(JoinOperator::Inner, None)] ); + // left join explicitly assert_eq!( only(&verified_only_select("SELECT * FROM t1 NATURAL LEFT JOIN t2").from).joins, + vec![natural_join(JoinOperator::Left, None)] + ); + + // left outer join explicitly + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 NATURAL LEFT OUTER JOIN t2").from).joins, vec![natural_join(JoinOperator::LeftOuter, None)] ); // right join explicitly assert_eq!( only(&verified_only_select("SELECT * FROM t1 NATURAL RIGHT JOIN t2").from).joins, + vec![natural_join(JoinOperator::Right, None)] + ); + + // right outer join explicitly + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 NATURAL RIGHT OUTER JOIN t2").from).joins, vec![natural_join(JoinOperator::RightOuter, None)] ); @@ -6625,7 +7534,7 @@ fn parse_natural_join() { // natural join another table with alias assert_eq!( only(&verified_only_select("SELECT * FROM t1 NATURAL JOIN t2 AS t3").from).joins, - vec![natural_join(JoinOperator::Inner, table_alias("t3"))] + vec![natural_join(JoinOperator::Join, table_alias("t3"))] ); let sql = "SELECT * FROM t1 natural"; @@ -6692,24 +7601,19 @@ fn parse_join_nesting() { #[test] fn parse_join_syntax_variants() { - one_statement_parses_to( - "SELECT c1 FROM t1 INNER JOIN t2 USING(c1)", - "SELECT c1 FROM t1 JOIN t2 USING(c1)", - ); - one_statement_parses_to( - "SELECT c1 FROM t1 LEFT OUTER JOIN t2 USING(c1)", - "SELECT c1 FROM t1 LEFT JOIN t2 USING(c1)", - ); - one_statement_parses_to( - "SELECT c1 FROM t1 RIGHT OUTER JOIN t2 USING(c1)", - "SELECT c1 FROM t1 RIGHT JOIN t2 USING(c1)", - ); + verified_stmt("SELECT c1 FROM t1 JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 INNER JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 LEFT JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 LEFT OUTER JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 RIGHT JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 RIGHT OUTER JOIN t2 USING(c1)"); one_statement_parses_to( "SELECT c1 FROM t1 FULL OUTER JOIN t2 USING(c1)", "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() @@ -6763,7 +7667,7 @@ fn parse_ctes() { // CTE in a view let sql = &format!("CREATE VIEW v AS {with}"); match verified_stmt(sql) { - Statement::CreateView { query, .. } => assert_ctes_in_select(&cte_sqls, &query), + Statement::CreateView(create_view) => assert_ctes_in_select(&cte_sqls, &create_view.query), _ => panic!("Expected: CREATE VIEW"), } // CTE in a CTE... @@ -6820,6 +7724,33 @@ fn parse_recursive_cte() { assert_eq!(with.cte_tables.first().unwrap(), &expected); } +#[test] +fn parse_cte_in_data_modification_statements() { + match verified_stmt("WITH x AS (SELECT 1) UPDATE t SET bar = (SELECT * FROM x)") { + Statement::Query(query) => { + assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 1)"); + assert!(matches!(*query.body, SetExpr::Update(_))); + } + 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)") { + Statement::Query(query) => { + 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:?}"), + } + + match verified_stmt("WITH x AS (SELECT 42) INSERT INTO t SELECT foo FROM x") { + Statement::Query(query) => { + assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 42)"); + assert!(matches!(*query.body, SetExpr::Insert(_))); + } + other => panic!("Expected: INSERT, got: {other:?}"), + } +} + #[test] fn parse_derived_tables() { let sql = "SELECT a.x, b.y FROM (SELECT x FROM foo) AS a CROSS JOIN (SELECT y FROM bar) AS b"; @@ -6856,9 +7787,9 @@ fn parse_derived_tables() { }), }, joins: vec![Join { - relation: table_from_name(ObjectName(vec!["t2".into()])), + relation: table_from_name(ObjectName::from(vec!["t2".into()])), global: false, - join_operator: JoinOperator::Inner(JoinConstraint::Natural), + join_operator: JoinOperator::Join(JoinConstraint::Natural), }], }), alias: None, @@ -6875,9 +7806,6 @@ fn parse_union_except_intersect_minus() { verified_stmt("SELECT 1 EXCEPT SELECT 2"); verified_stmt("SELECT 1 EXCEPT ALL SELECT 2"); verified_stmt("SELECT 1 EXCEPT DISTINCT SELECT 1"); - verified_stmt("SELECT 1 MINUS SELECT 2"); - verified_stmt("SELECT 1 MINUS ALL SELECT 2"); - verified_stmt("SELECT 1 MINUS DISTINCT SELECT 1"); verified_stmt("SELECT 1 INTERSECT SELECT 2"); verified_stmt("SELECT 1 INTERSECT ALL SELECT 2"); verified_stmt("SELECT 1 INTERSECT DISTINCT SELECT 1"); @@ -6896,6 +7824,13 @@ fn parse_union_except_intersect_minus() { verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT BY NAME SELECT 9 AS y, 8 AS x"); verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT ALL BY NAME SELECT 9 AS y, 8 AS x"); verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT DISTINCT BY NAME SELECT 9 AS y, 8 AS x"); + + // Dialects that support `MINUS` as column identifier + // do not support `MINUS` as a set operator. + let dialects = all_dialects_where(|d| !d.is_column_alias(&Keyword::MINUS, &mut Parser::new(d))); + dialects.verified_stmt("SELECT 1 MINUS SELECT 2"); + dialects.verified_stmt("SELECT 1 MINUS ALL SELECT 2"); + dialects.verified_stmt("SELECT 1 MINUS DISTINCT SELECT 1"); } #[test] @@ -6976,6 +7911,9 @@ fn parse_substring() { verified_stmt("SELECT SUBSTRING('1', 1, 3)"); verified_stmt("SELECT SUBSTRING('1', 1)"); verified_stmt("SELECT SUBSTRING('1' FOR 3)"); + verified_stmt("SELECT SUBSTRING('foo' FROM 1 FOR 2) FROM t"); + verified_stmt("SELECT SUBSTR('foo' FROM 1 FOR 2) FROM t"); + verified_stmt("SELECT SUBSTR('foo', 1, 2) FROM t"); } #[test] @@ -6997,13 +7935,15 @@ fn parse_overlay() { let select = verified_only_select(sql); assert_eq!( &Expr::Overlay { - expr: Box::new(Expr::Value(Value::SingleQuotedString("abcdef".to_string()))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("abcdef".to_string())).with_empty_span() + )), overlay_what: Box::new(Expr::Identifier(Ident::new("name"))), - overlay_from: Box::new(Expr::Value(number("3"))), + overlay_from: Box::new(Expr::value(number("3"))), overlay_for: Some(Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), })), }, expr_from_projection(only(&select.projection)) @@ -7054,7 +7994,6 @@ fn parse_trim() { Box::new(MySqlDialect {}), //Box::new(BigQueryDialect {}), Box::new(SQLiteDialect {}), - Box::new(DuckDbDialect {}), ]); assert_eq!( @@ -7119,11 +8058,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!(), } @@ -7138,11 +8101,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!(), } @@ -7189,7 +8155,8 @@ fn parse_drop_database_if_exists() { fn parse_create_view() { let sql = "CREATE VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { + or_alter, name, columns, query, @@ -7203,7 +8170,10 @@ fn parse_create_view() { temporary, to, params, - } => { + name_before_not_exists: _, + secure: _, + }) => { + assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -7220,25 +8190,29 @@ fn parse_create_view() { } _ => unreachable!(), } + + let _ = verified_stmt("CREATE OR ALTER VIEW v AS SELECT 1"); } #[test] fn parse_create_view_with_options() { let sql = "CREATE VIEW v WITH (foo = 'bar', a = 123) AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { options, .. } => { + Statement::CreateView(create_view) => { assert_eq!( CreateTableOptions::With(vec![ SqlOption::KeyValue { key: "foo".into(), - value: Expr::Value(Value::SingleQuotedString("bar".into())), + value: Expr::Value( + (Value::SingleQuotedString("bar".into())).with_empty_span() + ), }, SqlOption::KeyValue { key: "a".into(), - value: Expr::Value(number("123")), + value: Expr::value(number("123")), }, ]), - options + create_view.options ); } _ => unreachable!(), @@ -7251,21 +8225,22 @@ fn parse_create_view_with_columns() { // TODO: why does this fail for ClickHouseDialect? (#1449) // match all_dialects().verified_stmt(sql) { match all_dialects_except(|d| d.is::()).verified_stmt(sql) { - Statement::CreateView { - name, - columns, - or_replace, - options, - query, - materialized, - cluster_by, - comment, - with_no_schema_binding: late_binding, - if_not_exists, - temporary, - to, - params, - } => { + Statement::CreateView(create_view) => { + let or_alter = create_view.or_alter; + let name = create_view.name; + let columns = create_view.columns; + let or_replace = create_view.or_replace; + let options = create_view.options; + let query = create_view.query; + let materialized = create_view.materialized; + let cluster_by = create_view.cluster_by; + let comment = create_view.comment; + let late_binding = create_view.with_no_schema_binding; + let if_not_exists = create_view.if_not_exists; + let temporary = create_view.temporary; + let to = create_view.to; + let params = create_view.params; + assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!( columns, @@ -7274,7 +8249,7 @@ fn parse_create_view_with_columns() { .map(|name| ViewColumnDef { name, data_type: None, - options: None + options: None, }) .collect::>() ); @@ -7298,7 +8273,8 @@ fn parse_create_view_with_columns() { fn parse_create_view_temporary() { let sql = "CREATE TEMPORARY VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { + or_alter, name, columns, query, @@ -7312,7 +8288,10 @@ fn parse_create_view_temporary() { temporary, to, params, - } => { + name_before_not_exists: _, + secure: _, + }) => { + assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -7335,7 +8314,8 @@ fn parse_create_view_temporary() { fn parse_create_or_replace_view() { let sql = "CREATE OR REPLACE VIEW v AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { + or_alter, name, columns, or_replace, @@ -7349,7 +8329,10 @@ fn parse_create_or_replace_view() { temporary, to, params, - } => { + name_before_not_exists: _, + secure: _, + }) => { + assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); assert_eq!(options, CreateTableOptions::None); @@ -7376,7 +8359,8 @@ fn parse_create_or_replace_materialized_view() { // https://docs.snowflake.com/en/sql-reference/sql/create-materialized-view.html let sql = "CREATE OR REPLACE MATERIALIZED VIEW v AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { + or_alter, name, columns, or_replace, @@ -7390,7 +8374,10 @@ fn parse_create_or_replace_materialized_view() { temporary, to, params, - } => { + name_before_not_exists: _, + secure: _, + }) => { + assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); assert_eq!(options, CreateTableOptions::None); @@ -7413,7 +8400,8 @@ fn parse_create_or_replace_materialized_view() { fn parse_create_materialized_view() { let sql = "CREATE MATERIALIZED VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { + or_alter, name, or_replace, columns, @@ -7427,7 +8415,10 @@ fn parse_create_materialized_view() { temporary, to, params, - } => { + name_before_not_exists: _, + secure: _, + }) => { + assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -7450,7 +8441,8 @@ fn parse_create_materialized_view() { fn parse_create_materialized_view_with_cluster_by() { let sql = "CREATE MATERIALIZED VIEW myschema.myview CLUSTER BY (foo) AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { + or_alter, name, or_replace, columns, @@ -7464,7 +8456,10 @@ fn parse_create_materialized_view_with_cluster_by() { temporary, to, params, - } => { + name_before_not_exists: _, + secure: _, + }) => { + assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -7559,6 +8554,27 @@ fn parse_drop_view() { } _ => unreachable!(), } + + verified_stmt("DROP MATERIALIZED VIEW a.b.c"); + 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] @@ -7572,53 +8588,70 @@ fn parse_invalid_subquery_without_parens() { #[test] fn parse_offset() { - let expect = Some(Offset { - value: Expr::Value(number("2")), - rows: OffsetRows::Rows, + // Dialects that support `OFFSET` as column identifiers + // don't support this syntax. + let dialects = + all_dialects_where(|d| !d.is_column_alias(&Keyword::OFFSET, &mut Parser::new(d))); + + let expected_limit_clause = &Some(LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { + value: Expr::value(number("2")), + rows: OffsetRows::Rows, + }), + limit_by: vec![], }); - let ast = verified_query("SELECT foo FROM bar OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); - let ast = verified_query("SELECT foo FROM bar WHERE foo = 4 OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); - let ast = verified_query("SELECT foo FROM bar ORDER BY baz OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); - let ast = verified_query("SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); - let ast = verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS) OFFSET 2 ROWS"); - assert_eq!(ast.offset, expect); + let ast = dialects.verified_query("SELECT foo FROM bar OFFSET 2 ROWS"); + assert_eq!(&ast.limit_clause, expected_limit_clause); + let ast = dialects.verified_query("SELECT foo FROM bar WHERE foo = 4 OFFSET 2 ROWS"); + assert_eq!(&ast.limit_clause, expected_limit_clause); + let ast = dialects.verified_query("SELECT foo FROM bar ORDER BY baz OFFSET 2 ROWS"); + assert_eq!(&ast.limit_clause, expected_limit_clause); + let ast = + dialects.verified_query("SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS"); + assert_eq!(&ast.limit_clause, expected_limit_clause); + let ast = + dialects.verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS) OFFSET 2 ROWS"); + assert_eq!(&ast.limit_clause, expected_limit_clause); match *ast.body { SetExpr::Select(s) => match only(s.from).relation { TableFactor::Derived { subquery, .. } => { - assert_eq!(subquery.offset, expect); + assert_eq!(&subquery.limit_clause, expected_limit_clause); } _ => panic!("Test broke"), }, _ => panic!("Test broke"), } - let ast = verified_query("SELECT 'foo' OFFSET 0 ROWS"); - assert_eq!( - ast.offset, - Some(Offset { - value: Expr::Value(number("0")), + let expected_limit_clause = LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { + value: Expr::value(number("0")), rows: OffsetRows::Rows, - }) - ); - let ast = verified_query("SELECT 'foo' OFFSET 1 ROW"); - assert_eq!( - ast.offset, - Some(Offset { - value: Expr::Value(number("1")), + }), + limit_by: vec![], + }; + let ast = dialects.verified_query("SELECT 'foo' OFFSET 0 ROWS"); + assert_eq!(ast.limit_clause, Some(expected_limit_clause)); + let expected_limit_clause = LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { + value: Expr::value(number("1")), rows: OffsetRows::Row, - }) - ); - let ast = verified_query("SELECT 'foo' OFFSET 1"); - assert_eq!( - ast.offset, - Some(Offset { - value: Expr::Value(number("1")), + }), + limit_by: vec![], + }; + let ast = dialects.verified_query("SELECT 'foo' OFFSET 1 ROW"); + assert_eq!(ast.limit_clause, Some(expected_limit_clause)); + let expected_limit_clause = LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { + value: Expr::value(number("2")), rows: OffsetRows::None, - }) - ); + }), + limit_by: vec![], + }; + let ast = dialects.verified_query("SELECT 'foo' OFFSET 2"); + assert_eq!(ast.limit_clause, Some(expected_limit_clause)); } #[test] @@ -7626,7 +8659,7 @@ fn parse_fetch() { let fetch_first_two_rows_only = Some(Fetch { with_ties: false, percent: false, - quantity: Some(Expr::Value(number("2"))), + quantity: Some(Expr::value(number("2"))), }); let ast = verified_query("SELECT foo FROM bar FETCH FIRST 2 ROWS ONLY"); assert_eq!(ast.fetch, fetch_first_two_rows_only); @@ -7653,7 +8686,7 @@ fn parse_fetch() { Some(Fetch { with_ties: true, percent: false, - quantity: Some(Expr::Value(number("2"))), + quantity: Some(Expr::value(number("2"))), }) ); let ast = verified_query("SELECT foo FROM bar FETCH FIRST 50 PERCENT ROWS ONLY"); @@ -7662,19 +8695,21 @@ fn parse_fetch() { Some(Fetch { with_ties: false, percent: true, - quantity: Some(Expr::Value(number("50"))), + quantity: Some(Expr::value(number("50"))), }) ); let ast = verified_query( "SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY", ); - assert_eq!( - ast.offset, - Some(Offset { - value: Expr::Value(number("2")), + let expected_limit_clause = Some(LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { + value: Expr::value(number("2")), rows: OffsetRows::Rows, - }) - ); + }), + limit_by: vec![], + }); + assert_eq!(ast.limit_clause, expected_limit_clause); assert_eq!(ast.fetch, fetch_first_two_rows_only); let ast = verified_query( "SELECT foo FROM (SELECT * FROM bar FETCH FIRST 2 ROWS ONLY) FETCH FIRST 2 ROWS ONLY", @@ -7690,24 +8725,20 @@ fn parse_fetch() { _ => panic!("Test broke"), } let ast = verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY) OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY"); - assert_eq!( - ast.offset, - Some(Offset { - value: Expr::Value(number("2")), + let expected_limit_clause = &Some(LimitClause::LimitOffset { + limit: None, + offset: Some(Offset { + value: Expr::value(number("2")), rows: OffsetRows::Rows, - }) - ); + }), + limit_by: vec![], + }); + assert_eq!(&ast.limit_clause, expected_limit_clause); assert_eq!(ast.fetch, fetch_first_two_rows_only); match *ast.body { SetExpr::Select(s) => match only(s.from).relation { TableFactor::Derived { subquery, .. } => { - assert_eq!( - subquery.offset, - Some(Offset { - value: Expr::Value(number("2")), - rows: OffsetRows::Rows, - }) - ); + assert_eq!(&subquery.limit_clause, expected_limit_clause); assert_eq!(subquery.fetch, fetch_first_two_rows_only); } _ => panic!("Test broke"), @@ -7754,7 +8785,9 @@ fn lateral_derived() { let join = &from.joins[0]; assert_eq!( join.join_operator, - JoinOperator::LeftOuter(JoinConstraint::On(Expr::Value(test_utils::number("1")))) + JoinOperator::Left(JoinConstraint::On(Expr::Value( + (test_utils::number("1")).with_empty_span() + ))) ); if let TableFactor::Derived { lateral, @@ -7801,10 +8834,11 @@ fn lateral_function() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "customer".to_string(), quote_style: None, span: Span::empty(), @@ -7812,9 +8846,11 @@ fn lateral_function() { joins: vec![Join { relation: TableFactor::Function { lateral: true, - name: ObjectName(vec!["generate_series".into()]), + name: ObjectName::from(vec!["generate_series".into()]), args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("1")).with_empty_span(), + ))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::CompoundIdentifier( vec![Ident::new("customer"), Ident::new("id")], ))), @@ -7822,7 +8858,7 @@ fn lateral_function() { alias: None, }, global: false, - join_operator: JoinOperator::LeftOuter(JoinConstraint::None), + join_operator: JoinOperator::Left(JoinConstraint::None), }], }], lateral_views: vec![], @@ -7838,13 +8874,22 @@ fn lateral_function() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }; assert_eq!(actual_select_only, expected); } #[test] fn parse_start_transaction() { - match verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { + let dialects = all_dialects_except(|d| + // 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") + { Statement::StartTransaction { modes, .. } => assert_eq!( modes, vec![ @@ -7858,7 +8903,7 @@ fn parse_start_transaction() { // For historical reasons, PostgreSQL allows the commas between the modes to // be omitted. - match one_statement_parses_to( + match dialects.one_statement_parses_to( "START TRANSACTION READ ONLY READ WRITE ISOLATION LEVEL SERIALIZABLE", "START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE", ) { @@ -7873,40 +8918,40 @@ fn parse_start_transaction() { _ => unreachable!(), } - verified_stmt("START TRANSACTION"); - verified_stmt("BEGIN"); - verified_stmt("BEGIN WORK"); - verified_stmt("BEGIN TRANSACTION"); + dialects.verified_stmt("START TRANSACTION"); + dialects.verified_stmt("BEGIN"); + dialects.verified_stmt("BEGIN WORK"); + dialects.verified_stmt("BEGIN TRANSACTION"); - verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); - verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED"); - verified_stmt("START TRANSACTION ISOLATION LEVEL REPEATABLE READ"); - verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"); + dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); + dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED"); + dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL REPEATABLE READ"); + dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"); // Regression test for https://github.com/sqlparser-rs/sqlparser-rs/pull/139, // in which START TRANSACTION would fail to parse if followed by a statement // terminator. assert_eq!( - parse_sql_statements("START TRANSACTION; SELECT 1"), + dialects.parse_sql_statements("START TRANSACTION; SELECT 1"), Ok(vec![ verified_stmt("START TRANSACTION"), verified_stmt("SELECT 1"), ]) ); - let res = parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD"); + let res = dialects.parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD"); assert_eq!( ParserError::ParserError("Expected: isolation level, found: BAD".to_string()), res.unwrap_err() ); - let res = parse_sql_statements("START TRANSACTION BAD"); + let res = dialects.parse_sql_statements("START TRANSACTION BAD"); assert_eq!( ParserError::ParserError("Expected: end of statement, found: BAD".to_string()), res.unwrap_err() ); - let res = parse_sql_statements("START TRANSACTION READ ONLY,"); + let res = dialects.parse_sql_statements("START TRANSACTION READ ONLY,"); assert_eq!( ParserError::ParserError("Expected: transaction mode, found: EOF".to_string()), res.unwrap_err() @@ -7940,11 +8985,11 @@ fn parse_set_transaction() { // TRANSACTION, so no need to duplicate the tests here. We just do a quick // sanity check. match verified_stmt("SET TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { - Statement::SetTransaction { + Statement::Set(Set::SetTransaction { modes, session, snapshot, - } => { + }) => { assert_eq!( modes, vec![ @@ -7963,21 +9008,40 @@ fn parse_set_transaction() { #[test] fn parse_set_variable() { match verified_stmt("SET SOMETHING = '1'") { - Statement::SetVariable { - local, + Statement::Set(Set::SingleAssignment { + scope, hivevar, - variables, - value, - } => { - assert!(!local); + variable, + values, + }) => { + assert_eq!(scope, None); assert!(!hivevar); + assert_eq!(variable, ObjectName::from(vec!["SOMETHING".into()])); assert_eq!( - variables, - OneOrManyWithParens::One(ObjectName(vec!["SOMETHING".into()])) + values, + vec![Expr::Value( + (Value::SingleQuotedString("1".into())).with_empty_span() + )] ); + } + _ => unreachable!(), + } + + match verified_stmt("SET GLOBAL VARIABLE = 'Value'") { + Statement::Set(Set::SingleAssignment { + scope, + hivevar, + variable, + values, + }) => { + assert_eq!(scope, Some(ContextModifier::Global)); + assert!(!hivevar); + assert_eq!(variable, ObjectName::from(vec!["VARIABLE".into()])); assert_eq!( - value, - vec![Expr::Value(Value::SingleQuotedString("1".into()))] + values, + vec![Expr::Value( + (Value::SingleQuotedString("Value".into())).with_empty_span() + )] ); } _ => unreachable!(), @@ -7986,28 +9050,21 @@ fn parse_set_variable() { let multi_variable_dialects = all_dialects_where(|d| d.supports_parenthesized_set_variables()); let sql = r#"SET (a, b, c) = (1, 2, 3)"#; match multi_variable_dialects.verified_stmt(sql) { - Statement::SetVariable { - local, - hivevar, - variables, - value, - } => { - assert!(!local); - assert!(!hivevar); + Statement::Set(Set::ParenthesizedAssignments { variables, values }) => { assert_eq!( variables, - OneOrManyWithParens::Many(vec![ - ObjectName(vec!["a".into()]), - ObjectName(vec!["b".into()]), - ObjectName(vec!["c".into()]), - ]) + vec![ + ObjectName::from(vec!["a".into()]), + ObjectName::from(vec!["b".into()]), + ObjectName::from(vec!["c".into()]), + ] ); assert_eq!( - value, + values, vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ] ); } @@ -8063,21 +9120,20 @@ fn parse_set_variable() { #[test] fn parse_set_role_as_variable() { match verified_stmt("SET role = 'foobar'") { - Statement::SetVariable { - local, + Statement::Set(Set::SingleAssignment { + scope, hivevar, - variables, - value, - } => { - assert!(!local); + variable, + values, + }) => { + assert_eq!(scope, None); assert!(!hivevar); + assert_eq!(variable, ObjectName::from(vec!["role".into()])); assert_eq!( - variables, - OneOrManyWithParens::One(ObjectName(vec!["role".into()])) - ); - assert_eq!( - value, - vec![Expr::Value(Value::SingleQuotedString("foobar".into()))] + values, + vec![Expr::Value( + (Value::SingleQuotedString("foobar".into())).with_empty_span() + )] ); } _ => unreachable!(), @@ -8093,15 +9149,16 @@ fn parse_double_colon_cast_at_timezone() { &Expr::AtTimeZone { timestamp: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2001-01-01T00:00:00.000Z".to_string() - ),)), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2001-01-01T00:00:00.000Z".to_string())) + .with_empty_span() + )), data_type: DataType::Timestamp(None, TimezoneInfo::None), format: None }), - time_zone: Box::new(Expr::Value(Value::SingleQuotedString( - "Europe/Brussels".to_string() - ))), + time_zone: Box::new(Expr::Value( + (Value::SingleQuotedString("Europe/Brussels".to_string())).with_empty_span() + )), }, expr_from_projection(only(&select.projection)), ); @@ -8110,21 +9167,20 @@ fn parse_double_colon_cast_at_timezone() { #[test] fn parse_set_time_zone() { match verified_stmt("SET TIMEZONE = 'UTC'") { - Statement::SetVariable { - local, + Statement::Set(Set::SingleAssignment { + scope, hivevar, - variables: variable, - value, - } => { - assert!(!local); + variable, + values, + }) => { + assert_eq!(scope, None); assert!(!hivevar); + assert_eq!(variable, ObjectName::from(vec!["TIMEZONE".into()])); assert_eq!( - variable, - OneOrManyWithParens::One(ObjectName(vec!["TIMEZONE".into()])) - ); - assert_eq!( - value, - vec![Expr::Value(Value::SingleQuotedString("UTC".into()))] + values, + vec![Expr::Value( + (Value::SingleQuotedString("UTC".into())).with_empty_span() + )] ); } _ => unreachable!(), @@ -8133,17 +9189,6 @@ fn parse_set_time_zone() { one_statement_parses_to("SET TIME ZONE TO 'UTC'", "SET TIMEZONE = 'UTC'"); } -#[test] -fn parse_set_time_zone_alias() { - match verified_stmt("SET TIME ZONE 'UTC'") { - Statement::SetTimeZone { local, value } => { - assert!(!local); - assert_eq!(value, Expr::Value(Value::SingleQuotedString("UTC".into()))); - } - _ => unreachable!(), - } -} - #[test] fn parse_commit() { match verified_stmt("COMMIT") { @@ -8243,19 +9288,29 @@ 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 indexed_columns = vec![ - OrderByExpr { - expr: Expr::Identifier(Ident::new("name")), - asc: None, - nulls_first: None, - with_fill: None, + let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test(name, age DESC)"; + let indexed_columns: Vec = vec![ + IndexColumn { + operator_class: None, + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("name")), + with_fill: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }, }, - OrderByExpr { - expr: Expr::Identifier(Ident::new("age")), - asc: Some(false), - nulls_first: None, - with_fill: None, + IndexColumn { + operator_class: None, + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("age")), + with_fill: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, + }, }, ]; match verified_stmt(sql) { @@ -8279,19 +9334,29 @@ 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 indexed_columns = vec![ - OrderByExpr { - expr: Expr::Identifier(Ident::new("name")), - asc: None, - nulls_first: None, - with_fill: None, + 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, + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("name")), + with_fill: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }, }, - OrderByExpr { - expr: Expr::Identifier(Ident::new("age")), - asc: Some(false), - nulls_first: None, - with_fill: None, + IndexColumn { + operator_class: None, + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("age")), + with_fill: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, + }, }, ]; match verified_stmt(sql) { @@ -8307,16 +9372,20 @@ 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()); - assert_eq!("btree", using.unwrap().to_string()); + assert_eq!("BTREE", using.unwrap().to_string()); assert_eq!(indexed_columns, columns); assert!(unique); assert!(!concurrently); assert!(if_not_exists); assert!(include.is_empty()); assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); } _ => unreachable!(), } @@ -8325,17 +9394,22 @@ fn test_create_index_with_using_function() { #[test] fn test_create_index_with_with_clause() { let sql = "CREATE UNIQUE INDEX title_idx ON films(title) WITH (fillfactor = 70, single_param)"; - let indexed_columns = vec![OrderByExpr { - expr: Expr::Identifier(Ident::new("title")), - asc: None, - nulls_first: None, - with_fill: None, + let indexed_columns: Vec = vec![IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(Ident::new("title")), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: None, }]; let with_parameters = vec![ Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("fillfactor"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("70"))), + right: Box::new(Expr::value(number("70"))), }, Expr::Identifier(Ident::new("single_param")), ]; @@ -8353,6 +9427,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()); @@ -8362,6 +9438,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!(), } @@ -8388,21 +9466,17 @@ fn parse_drop_index() { fn parse_create_role() { let sql = "CREATE ROLE consultant"; match verified_stmt(sql) { - Statement::CreateRole { names, .. } => { - assert_eq_vec(&["consultant"], &names); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["consultant"], &create_role.names); } _ => unreachable!(), } let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; match verified_stmt(sql) { - Statement::CreateRole { - names, - if_not_exists, - .. - } => { - assert_eq_vec(&["mysql_a", "mysql_b"], &names); - assert!(if_not_exists); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["mysql_a", "mysql_b"], &create_role.names); + assert!(create_role.if_not_exists); } _ => unreachable!(), } @@ -8443,7 +9517,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, @@ -8453,7 +9527,7 @@ fn parse_grant() { granted_by, .. } => match (privileges, objects) { - (Privileges::Actions(actions), GrantObjects::Tables(objects)) => { + (Privileges::Actions(actions), Some(GrantObjects::Tables(objects))) => { assert_eq!( vec![ Action::Select { columns: None }, @@ -8478,9 +9552,10 @@ fn parse_grant() { Action::References { columns: None }, Action::Trigger, Action::Connect, - Action::Create, - Action::Execute, + Action::Create { obj_type: None }, + Action::Execute { obj_type: None }, Action::Temporary, + Action::Drop, ], actions ); @@ -8503,7 +9578,7 @@ fn parse_grant() { with_grant_option, .. } => match (privileges, objects) { - (Privileges::Actions(actions), GrantObjects::AllTablesInSchema { schemas }) => { + (Privileges::Actions(actions), Some(GrantObjects::AllTablesInSchema { schemas })) => { assert_eq!(vec![Action::Insert { columns: None }], actions); assert_eq_vec(&["public"], &schemas); assert_eq_vec(&["browser"], &grantees); @@ -8523,7 +9598,7 @@ fn parse_grant() { granted_by, .. } => match (privileges, objects, granted_by) { - (Privileges::Actions(actions), GrantObjects::Sequences(objects), None) => { + (Privileges::Actions(actions), Some(GrantObjects::Sequences(objects)), None) => { assert_eq!( vec![Action::Usage, Action::Select { columns: None }], actions @@ -8560,7 +9635,7 @@ fn parse_grant() { Privileges::All { with_privileges_keyword, }, - GrantObjects::Schemas(schemas), + Some(GrantObjects::Schemas(schemas)), ) => { assert!(!with_privileges_keyword); assert_eq_vec(&["aa", "b"], &schemas); @@ -8577,7 +9652,10 @@ fn parse_grant() { objects, .. } => match (privileges, objects) { - (Privileges::Actions(actions), GrantObjects::AllSequencesInSchema { schemas }) => { + ( + Privileges::Actions(actions), + Some(GrantObjects::AllSequencesInSchema { schemas }), + ) => { assert_eq!(vec![Action::Usage], actions); assert_eq_vec(&["bus"], &schemas); } @@ -8592,8 +9670,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] @@ -8602,7 +9734,7 @@ fn test_revoke() { match verified_stmt(sql) { Statement::Revoke { privileges, - objects: GrantObjects::Tables(tables), + objects: Some(GrantObjects::Tables(tables)), grantees, granted_by, cascade, @@ -8628,7 +9760,7 @@ fn test_revoke_with_cascade() { match all_dialects_except(|d| d.is::()).verified_stmt(sql) { Statement::Revoke { privileges, - objects: GrantObjects::Tables(tables), + objects: Some(GrantObjects::Tables(tables)), grantees, granted_by, cascade, @@ -8660,6 +9792,7 @@ fn parse_merge() { source, on, clauses, + .. }, Statement::Merge { into: no_into, @@ -8667,6 +9800,7 @@ fn parse_merge() { source: source_no_into, on: on_no_into, clauses: clauses_no_into, + .. }, ) => { assert!(into); @@ -8675,7 +9809,7 @@ fn parse_merge() { assert_eq!( table, TableFactor::Table { - name: ObjectName(vec![Ident::new("s"), Ident::new("bar")]), + name: ObjectName::from(vec![Ident::new("s"), Ident::new("bar")]), alias: Some(TableAlias { name: Ident::new("dest"), columns: vec![], @@ -8687,6 +9821,7 @@ fn parse_merge() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], } ); assert_eq!(table, table_no_into); @@ -8705,9 +9840,10 @@ fn parse_merge() { projection: vec![SelectItem::Wildcard( WildcardAdditionalOptions::default() )], + exclude: None, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![ + relation: table_from_name(ObjectName::from(vec![ Ident::new("s"), Ident::new("foo") ])), @@ -8726,16 +9862,16 @@ fn parse_merge() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), alias: Some(TableAlias { name: Ident { @@ -8788,6 +9924,7 @@ fn parse_merge() { action: MergeAction::Insert(MergeInsertExpr { columns: vec![Ident::new("A"), Ident::new("B"), Ident::new("C")], kind: MergeInsertKind::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::CompoundIdentifier(vec![ @@ -8814,14 +9951,14 @@ fn parse_merge() { Ident::new("A"), ])), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "a".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("a".to_string())).with_empty_span() + )), }), action: MergeAction::Update { assignments: vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![ + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ Ident::new("dest"), Ident::new("F") ])), @@ -8831,7 +9968,7 @@ fn parse_merge() { ]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![ + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ Ident::new("dest"), Ident::new("G") ])), @@ -8859,6 +9996,50 @@ 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 \ + ON target_table.id = source_table.oooid \ + WHEN MATCHED THEN \ + UPDATE SET target_table.description = source_table.description \ + WHEN NOT MATCHED THEN \ + INSERT (ID, description) VALUES (source_table.id, source_table.description) \ + OUTPUT inserted.* INTO log_target"; + + 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 \ @@ -8938,12 +10119,12 @@ fn test_lock_table() { let lock = ast.locks.pop().unwrap(); assert_eq!(lock.lock_type, LockType::Update); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert!(lock.nonblock.is_none()); @@ -8953,12 +10134,12 @@ fn test_lock_table() { let lock = ast.locks.pop().unwrap(); assert_eq!(lock.lock_type, LockType::Share); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert!(lock.nonblock.is_none()); @@ -8968,23 +10149,23 @@ fn test_lock_table() { let lock = ast.locks.remove(0); assert_eq!(lock.lock_type, LockType::Share); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert!(lock.nonblock.is_none()); let lock = ast.locks.remove(0); assert_eq!(lock.lock_type, LockType::Update); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "student".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert!(lock.nonblock.is_none()); } @@ -8997,12 +10178,12 @@ fn test_lock_nonblock() { let lock = ast.locks.pop().unwrap(); assert_eq!(lock.lock_type, LockType::Update); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert_eq!(lock.nonblock.unwrap(), NonBlock::SkipLocked); @@ -9012,12 +10193,12 @@ fn test_lock_nonblock() { let lock = ast.locks.pop().unwrap(); assert_eq!(lock.lock_type, LockType::Share); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert_eq!(lock.nonblock.unwrap(), NonBlock::Nowait); } @@ -9042,23 +10223,24 @@ fn test_placeholder() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Placeholder("$Id1".into()))), + right: Box::new(Expr::Value( + (Value::Placeholder("$Id1".into())).with_empty_span() + )), }) ); - let sql = "SELECT * FROM student LIMIT $1 OFFSET $2"; - let ast = dialects.verified_query(sql); - assert_eq!( - ast.limit, - Some(Expr::Value(Value::Placeholder("$1".into()))) - ); - assert_eq!( - ast.offset, - Some(Offset { - value: Expr::Value(Value::Placeholder("$2".into())), + let ast = dialects.verified_query("SELECT * FROM student LIMIT $1 OFFSET $2"); + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::Value( + (Value::Placeholder("$1".into())).with_empty_span(), + )), + offset: Some(Offset { + value: Expr::Value((Value::Placeholder("$2".into())).with_empty_span()), rows: OffsetRows::None, }), - ); + limit_by: vec![], + }; + assert_eq!(ast.limit_clause, Some(expected_limit_clause)); let dialects = TestedDialects::new(vec![ Box::new(GenericDialect {}), @@ -9079,7 +10261,9 @@ fn test_placeholder() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Placeholder("?".into()))), + right: Box::new(Expr::Value( + (Value::Placeholder("?".into())).with_empty_span() + )), }) ); @@ -9088,9 +10272,15 @@ fn test_placeholder() { assert_eq!( ast.projection, vec![ - UnnamedExpr(Expr::Value(Value::Placeholder("$fromage_français".into()))), - UnnamedExpr(Expr::Value(Value::Placeholder(":x".into()))), - UnnamedExpr(Expr::Value(Value::Placeholder("?123".into()))), + UnnamedExpr(Expr::Value( + (Value::Placeholder("$fromage_français".into())).with_empty_span() + )), + UnnamedExpr(Expr::Value( + (Value::Placeholder(":x".into())).with_empty_span() + )), + UnnamedExpr(Expr::Value( + (Value::Placeholder("?123".into())).with_empty_span() + )), ] ); } @@ -9130,48 +10320,47 @@ fn verified_expr(query: &str) -> Expr { #[test] fn parse_offset_and_limit() { let sql = "SELECT foo FROM bar LIMIT 1 OFFSET 2"; - let expect = Some(Offset { - value: Expr::Value(number("2")), - rows: OffsetRows::None, + let expected_limit_clause = Some(LimitClause::LimitOffset { + limit: Some(Expr::value(number("1"))), + offset: Some(Offset { + value: Expr::value(number("2")), + rows: OffsetRows::None, + }), + limit_by: vec![], }); let ast = verified_query(sql); - assert_eq!(ast.offset, expect); - assert_eq!(ast.limit, Some(Expr::Value(number("1")))); + assert_eq!(ast.limit_clause, expected_limit_clause); // different order is OK one_statement_parses_to("SELECT foo FROM bar OFFSET 2 LIMIT 1", sql); // mysql syntax is ok for some dialects - TestedDialects::new(vec![ - Box::new(GenericDialect {}), - Box::new(MySqlDialect {}), - Box::new(SQLiteDialect {}), - Box::new(ClickHouseDialect {}), - ]) - .one_statement_parses_to("SELECT foo FROM bar LIMIT 2, 1", sql); + all_dialects_where(|d| d.supports_limit_comma()) + .verified_query("SELECT foo FROM bar LIMIT 2, 1"); // expressions are allowed let sql = "SELECT foo FROM bar LIMIT 1 + 2 OFFSET 3 * 4"; let ast = verified_query(sql); - assert_eq!( - ast.limit, - Some(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + let expected_limit_clause = LimitClause::LimitOffset { + limit: Some(Expr::BinaryOp { + left: Box::new(Expr::value(number("1"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("2"))), + right: Box::new(Expr::value(number("2"))), }), - ); - assert_eq!( - ast.offset, - Some(Offset { + offset: Some(Offset { value: Expr::BinaryOp { - left: Box::new(Expr::Value(number("3"))), + left: Box::new(Expr::value(number("3"))), op: BinaryOperator::Multiply, - right: Box::new(Expr::Value(number("4"))), + right: Box::new(Expr::value(number("4"))), }, rows: OffsetRows::None, }), - ); + limit_by: vec![], + }; + assert_eq!(ast.limit_clause, Some(expected_limit_clause),); + + // OFFSET without LIMIT + verified_stmt("SELECT foo FROM bar OFFSET 2"); // Can't repeat OFFSET / LIMIT let res = parse_sql_statements("SELECT foo FROM bar OFFSET 2 OFFSET 2"); @@ -9196,10 +10385,10 @@ 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(vec![Ident::new(func_name)]), + name: ObjectName::from(vec![Ident::new(func_name)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -9218,7 +10407,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!( @@ -9238,7 +10427,9 @@ fn parse_time_functions() { fn parse_position() { assert_eq!( Expr::Position { - expr: Box::new(Expr::Value(Value::SingleQuotedString("@".to_string()))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("@".to_string())).with_empty_span() + )), r#in: Box::new(Expr::Identifier(Ident::new("field"))), }, verified_expr("POSITION('@' IN field)"), @@ -9249,9 +10440,9 @@ fn parse_position() { call( "position", [ - Expr::Value(Value::SingleQuotedString("an".to_owned())), - Expr::Value(Value::SingleQuotedString("banana".to_owned())), - Expr::Value(number("1")), + Expr::Value((Value::SingleQuotedString("an".to_owned())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("banana".to_owned())).with_empty_span()), + Expr::value(number("1")), ] ), verified_expr("position('an', 'banana', 1)") @@ -9296,6 +10487,46 @@ fn parse_is_boolean() { verified_expr(sql) ); + let sql = "a IS NORMALIZED"; + assert_eq!( + IsNormalized { + expr: Box::new(Identifier(Ident::new("a"))), + form: None, + negated: false, + }, + verified_expr(sql) + ); + + let sql = "a IS NOT NORMALIZED"; + assert_eq!( + IsNormalized { + expr: Box::new(Identifier(Ident::new("a"))), + form: None, + negated: true, + }, + verified_expr(sql) + ); + + let sql = "a IS NFKC NORMALIZED"; + assert_eq!( + IsNormalized { + expr: Box::new(Identifier(Ident::new("a"))), + form: Some(NormalizationForm::NFKC), + negated: false, + }, + verified_expr(sql) + ); + + let sql = "a IS NOT NFKD NORMALIZED"; + assert_eq!( + IsNormalized { + expr: Box::new(Identifier(Ident::new("a"))), + form: Some(NormalizationForm::NFKD), + negated: true, + }, + verified_expr(sql) + ); + let sql = "a IS UNKNOWN"; assert_eq!( IsUnknown(Box::new(Identifier(Ident::new("a")))), @@ -9314,6 +10545,12 @@ fn parse_is_boolean() { verified_stmt("SELECT f FROM foo WHERE field IS FALSE"); verified_stmt("SELECT f FROM foo WHERE field IS NOT FALSE"); + verified_stmt("SELECT f FROM foo WHERE field IS NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS NFC NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS NFD NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS NOT NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS NOT NFKC NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS UNKNOWN"); verified_stmt("SELECT f FROM foo WHERE field IS NOT UNKNOWN"); @@ -9321,7 +10558,37 @@ fn parse_is_boolean() { let res = parse_sql_statements(sql); assert_eq!( ParserError::ParserError( - "Expected: [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: 0" + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: 0" + .to_string() + ), + res.unwrap_err() + ); + + let sql = "SELECT s, s IS XYZ NORMALIZED FROM foo"; + let res = parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: XYZ" + .to_string() + ), + res.unwrap_err() + ); + + let sql = "SELECT s, s IS NFKC FROM foo"; + let res = parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: FROM" + .to_string() + ), + res.unwrap_err() + ); + + let sql = "SELECT s, s IS TRIM(' NFKC ') FROM foo"; + let res = parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: TRIM" .to_string() ), res.unwrap_err() @@ -9396,7 +10663,7 @@ fn parse_cache_table() { verified_stmt(format!("CACHE TABLE '{cache_table_name}'").as_str()), Statement::Cache { table_flag: None, - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![], query: None, @@ -9406,8 +10673,8 @@ fn parse_cache_table() { assert_eq!( verified_stmt(format!("CACHE {table_flag} TABLE '{cache_table_name}'").as_str()), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![], query: None, @@ -9422,17 +10689,17 @@ fn parse_cache_table() { .as_str() ), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![ SqlOption::KeyValue { key: Ident::with_quote('\'', "K1"), - value: Expr::Value(Value::SingleQuotedString("V1".into())), + value: Expr::Value((Value::SingleQuotedString("V1".into())).with_empty_span()), }, SqlOption::KeyValue { key: Ident::with_quote('\'', "K2"), - value: Expr::Value(number("0.88")), + value: Expr::value(number("0.88")), }, ], query: None, @@ -9447,17 +10714,17 @@ fn parse_cache_table() { .as_str() ), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![ SqlOption::KeyValue { key: Ident::with_quote('\'', "K1"), - value: Expr::Value(Value::SingleQuotedString("V1".into())), + value: Expr::Value((Value::SingleQuotedString("V1".into())).with_empty_span()), }, SqlOption::KeyValue { key: Ident::with_quote('\'', "K2"), - value: Expr::Value(number("0.88")), + value: Expr::value(number("0.88")), }, ], query: Some(query.clone().into()), @@ -9472,17 +10739,17 @@ fn parse_cache_table() { .as_str() ), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: true, options: vec![ SqlOption::KeyValue { key: Ident::with_quote('\'', "K1"), - value: Expr::Value(Value::SingleQuotedString("V1".into())), + value: Expr::Value((Value::SingleQuotedString("V1".into())).with_empty_span()), }, SqlOption::KeyValue { key: Ident::with_quote('\'', "K2"), - value: Expr::Value(number("0.88")), + value: Expr::value(number("0.88")), }, ], query: Some(query.clone().into()), @@ -9492,8 +10759,8 @@ fn parse_cache_table() { assert_eq!( verified_stmt(format!("CACHE {table_flag} TABLE '{cache_table_name}' {sql}").as_str()), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![], query: Some(query.clone().into()), @@ -9503,8 +10770,8 @@ fn parse_cache_table() { assert_eq!( verified_stmt(format!("CACHE {table_flag} TABLE '{cache_table_name}' AS {sql}").as_str()), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: true, options: vec![], query: Some(query.into()), @@ -9567,7 +10834,7 @@ fn parse_uncache_table() { assert_eq!( verified_stmt("UNCACHE TABLE 'table_name'"), Statement::UNCache { - table_name: ObjectName(vec![Ident::with_quote('\'', "table_name")]), + table_name: ObjectName::from(vec![Ident::with_quote('\'', "table_name")]), if_exists: false, } ); @@ -9575,7 +10842,7 @@ fn parse_uncache_table() { assert_eq!( verified_stmt("UNCACHE TABLE IF EXISTS 'table_name'"), Statement::UNCache { - table_name: ObjectName(vec![Ident::with_quote('\'', "table_name")]), + table_name: ObjectName::from(vec![Ident::with_quote('\'', "table_name")]), if_exists: true, } ); @@ -9679,21 +10946,16 @@ fn parse_with_recursion_limit() { #[test] fn parse_escaped_string_with_unescape() { - fn assert_mysql_query_value(sql: &str, quoted: &str) { - let stmt = TestedDialects::new(vec![ - Box::new(MySqlDialect {}), - Box::new(BigQueryDialect {}), - Box::new(SnowflakeDialect {}), - ]) - .one_statement_parses_to(sql, ""); - - match stmt { + fn assert_mysql_query_value(dialects: &TestedDialects, sql: &str, quoted: &str) { + match dialects.one_statement_parses_to(sql, "") { Statement::Query(query) => match *query.body { SetExpr::Select(value) => { let expr = expr_from_projection(only(&value.projection)); assert_eq!( *expr, - Expr::Value(Value::SingleQuotedString(quoted.to_string())) + Expr::Value( + (Value::SingleQuotedString(quoted.to_string())).with_empty_span() + ) ); } _ => unreachable!(), @@ -9701,17 +10963,38 @@ fn parse_escaped_string_with_unescape() { _ => unreachable!(), }; } + + let escaping_dialects = + &all_dialects_where(|dialect| dialect.supports_string_literal_backslash_escape()); + let no_wildcard_exception = &all_dialects_where(|dialect| { + dialect.supports_string_literal_backslash_escape() && !dialect.ignores_wildcard_escapes() + }); + let with_wildcard_exception = &all_dialects_where(|dialect| { + dialect.supports_string_literal_backslash_escape() && dialect.ignores_wildcard_escapes() + }); + let sql = r"SELECT 'I\'m fine'"; - assert_mysql_query_value(sql, "I'm fine"); + assert_mysql_query_value(escaping_dialects, sql, "I'm fine"); let sql = r#"SELECT 'I''m fine'"#; - assert_mysql_query_value(sql, "I'm fine"); + assert_mysql_query_value(escaping_dialects, sql, "I'm fine"); let sql = r#"SELECT 'I\"m fine'"#; - assert_mysql_query_value(sql, "I\"m fine"); + assert_mysql_query_value(escaping_dialects, sql, "I\"m fine"); let sql = r"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \h \ '"; - assert_mysql_query_value(sql, "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} \u{7} h "); + assert_mysql_query_value( + no_wildcard_exception, + sql, + "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} \u{7} h ", + ); + + // check MySQL doesn't remove backslash from escaped LIKE wildcards + assert_mysql_query_value( + with_wildcard_exception, + sql, + "Testing: \0 \\ \\% \\_ \u{8} \n \r \t \u{1a} \u{7} h ", + ); } #[test] @@ -9733,7 +11016,9 @@ fn parse_escaped_string_without_unescape() { let expr = expr_from_projection(only(&value.projection)); assert_eq!( *expr, - Expr::Value(Value::SingleQuotedString(quoted.to_string())) + Expr::Value( + (Value::SingleQuotedString(quoted.to_string())).with_empty_span() + ) ); } _ => unreachable!(), @@ -9782,7 +11067,7 @@ fn parse_pivot_table() { verified_only_select(sql).from[0].relation, Pivot { table: Box::new(TableFactor::Table { - name: ObjectName(vec![Ident::new("monthly_sales")]), + name: ObjectName::from(vec![Ident::new("monthly_sales")]), alias: Some(TableAlias { name: Ident::new("a"), columns: vec![] @@ -9794,20 +11079,26 @@ fn parse_pivot_table() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }), aggregate_functions: vec![ expected_function("a", None), 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")), + expr: Expr::value(number("1")), alias: Some(Ident::new("x")) }, ExprWithAlias { - expr: Expr::Value(Value::SingleQuotedString("two".to_string())), + expr: Expr::Value( + (Value::SingleQuotedString("two".to_string())).with_empty_span() + ), alias: None }, ExprWithAlias { @@ -9845,24 +11136,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(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, @@ -9870,41 +11157,107 @@ fn parse_unpivot_table() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }), - value: Ident { - value: "quantity".to_string(), - quote_style: None, - span: Span::empty() - }, - - name: Ident { - value: "quarter".to_string(), - quote_style: None, - span: Span::empty() - }, - columns: ["Q1", "Q2", "Q3", "Q4"] - .into_iter() - .map(Ident::new) - .collect(), - alias: Some(TableAlias { - name: Ident::new("u"), - columns: ["product", "quarter", "quantity"] - .into_iter() - .map(TableAliasColumnDef::from_name) - .collect(), - }), - } - ); - assert_eq!(verified_stmt(sql).to_string(), sql); - - let sql_without_aliases = concat!( - "SELECT * FROM sales ", - "UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4))" - ); - - assert_matches!( - &verified_only_select(sql_without_aliases).from[0].relation, - Unpivot { + 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 + ); +} + +#[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("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!( + "SELECT * FROM sales ", + "UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4))" + ); + + assert_matches!( + &verified_only_select(sql_without_aliases).from[0].relation, + Unpivot { table, alias: None, .. @@ -9914,6 +11267,228 @@ 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] +fn parse_select_table_with_index_hints() { + let supported_dialects = all_dialects_where(|d| d.supports_table_hints()); + let s = supported_dialects.verified_only_select( + "SELECT * FROM t1 USE INDEX (i1) IGNORE INDEX FOR ORDER BY (i2) ORDER BY a", + ); + if let TableFactor::Table { index_hints, .. } = &s.from[0].relation { + assert_eq!( + vec![ + TableIndexHints { + hint_type: TableIndexHintType::Use, + index_names: vec!["i1".into()], + index_type: TableIndexType::Index, + for_clause: None, + }, + TableIndexHints { + hint_type: TableIndexHintType::Ignore, + index_names: vec!["i2".into()], + index_type: TableIndexType::Index, + for_clause: Some(TableIndexHintForClause::OrderBy), + }, + ], + *index_hints + ); + } else { + panic!("Expected TableFactor::Table"); + } + supported_dialects.verified_stmt("SELECT * FROM t1 USE INDEX (i1) USE INDEX (i1, i1)"); + supported_dialects.verified_stmt( + "SELECT * FROM t1 USE INDEX () IGNORE INDEX (i2) USE INDEX (i1) USE INDEX (i2)", + ); + supported_dialects.verified_stmt("SELECT * FROM t1 FORCE INDEX FOR JOIN (i2)"); + supported_dialects.verified_stmt("SELECT * FROM t1 IGNORE INDEX FOR JOIN (i2)"); + supported_dialects.verified_stmt( + "SELECT * FROM t USE INDEX (index1) IGNORE INDEX FOR ORDER BY (index1) IGNORE INDEX FOR GROUP BY (index1) WHERE A = B", + ); + + // Test that dialects that don't support table hints will keep parsing the USE as table alias + let sql = "SELECT * FROM T USE LIMIT 1"; + let unsupported_dialects = all_dialects_where(|d| !d.supports_table_hints()); + let select = unsupported_dialects + .verified_only_select_with_canonical(sql, "SELECT * FROM T AS USE LIMIT 1"); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![sqlparser::ast::ObjectNamePart::Identifier( + Ident::new("T") + )]), + alias: Some(TableAlias { + name: Ident::new("USE"), + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![], + }] + ); } #[test] @@ -9929,7 +11504,7 @@ fn parse_pivot_unpivot_table() { Pivot { table: Box::new(Unpivot { table: Box::new(TableFactor::Table { - name: ObjectName(vec![Ident::new("census")]), + name: ObjectName::from(vec![Ident::new("census")]), alias: Some(TableAlias { name: Ident::new("c"), columns: vec![] @@ -9941,21 +11516,17 @@ fn parse_pivot_unpivot_table() { with_ordinality: false, json_path: None, 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"), @@ -9966,14 +11537,20 @@ 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(Value::SingleQuotedString("population_2000".to_string())), + expr: Expr::Value( + (Value::SingleQuotedString("population_2000".to_string())) + .with_empty_span() + ), alias: None }, ExprWithAlias { - expr: Expr::Value(Value::SingleQuotedString("population_2010".to_string())), + expr: Expr::Value( + (Value::SingleQuotedString("population_2010".to_string())) + .with_empty_span() + ), alias: None }, ]), @@ -10014,10 +11591,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()); @@ -10066,7 +11648,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 @@ -10090,15 +11672,19 @@ fn parse_trailing_comma() { "Expected: column name or constraint definition, found: )".to_string() ) ); + + let unsupported_dialects = all_dialects_where(|d| !d.supports_trailing_commas()); + assert_eq!( + unsupported_dialects + .parse_sql_statements("SELECT * FROM track ORDER BY milliseconds,") + .unwrap_err(), + ParserError::ParserError("Expected: an expression, found: EOF".to_string()) + ); } #[test] fn parse_projection_trailing_comma() { - // Some dialects allow trailing commas only in the projection - let trailing_commas = TestedDialects::new(vec![ - Box::new(SnowflakeDialect {}), - Box::new(BigQueryDialect {}), - ]); + let trailing_commas = all_dialects_where(|d| d.supports_projection_trailing_commas()); trailing_commas.one_statement_parses_to( "SELECT album_id, name, FROM track", @@ -10111,20 +11697,14 @@ fn parse_projection_trailing_comma() { trailing_commas.verified_stmt("SELECT DISTINCT ON (album_id) name FROM track"); + let unsupported_dialects = all_dialects_where(|d| { + !d.supports_projection_trailing_commas() && !d.supports_trailing_commas() + }); assert_eq!( - trailing_commas - .parse_sql_statements("SELECT * FROM track ORDER BY milliseconds,") - .unwrap_err(), - ParserError::ParserError("Expected: an expression, found: EOF".to_string()) - ); - - assert_eq!( - trailing_commas - .parse_sql_statements("CREATE TABLE employees (name text, age int,)") + unsupported_dialects + .parse_sql_statements("SELECT album_id, name, FROM track") .unwrap_err(), - ParserError::ParserError( - "Expected: column name or constraint definition, found: )".to_string() - ), + ParserError::ParserError("Expected an expression, found: FROM".to_string()) ); } @@ -10134,7 +11714,7 @@ fn parse_create_type() { verified_stmt("CREATE TYPE db.type_name AS (foo INT, bar TEXT COLLATE \"de_DE\")"); assert_eq!( Statement::CreateType { - name: ObjectName(vec![Ident::new("db"), Ident::new("type_name")]), + name: ObjectName::from(vec![Ident::new("db"), Ident::new("type_name")]), representation: UserDefinedTypeRepresentation::Composite { attributes: vec![ UserDefinedTypeCompositeAttributeDef { @@ -10145,7 +11725,7 @@ fn parse_create_type() { UserDefinedTypeCompositeAttributeDef { name: Ident::new("bar"), data_type: DataType::Text, - collation: Some(ObjectName(vec![Ident::with_quote('\"', "de_DE")])), + collation: Some(ObjectName::from(vec![Ident::with_quote('\"', "de_DE")])), } ] } @@ -10222,11 +11802,11 @@ fn parse_call() { args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".to_string()) + (Value::SingleQuotedString("a".to_string())).with_empty_span() )))], clauses: vec![], }), - name: ObjectName(vec![Ident::new("my_procedure")]), + name: ObjectName::from(vec![Ident::new("my_procedure")]), filter: None, null_treatment: None, over: None, @@ -10238,7 +11818,7 @@ fn parse_call() { #[test] fn parse_execute_stored_procedure() { let expected = Statement::Execute { - name: ObjectName(vec![ + name: Some(ObjectName::from(vec![ Ident { value: "my_schema".to_string(), quote_style: None, @@ -10249,13 +11829,17 @@ fn parse_execute_stored_procedure() { quote_style: None, span: Span::empty(), }, - ]), + ])), parameters: vec![ - Expr::Value(Value::NationalStringLiteral("param1".to_string())), - Expr::Value(Value::NationalStringLiteral("param2".to_string())), + Expr::Value((Value::NationalStringLiteral("param1".to_string())).with_empty_span()), + Expr::Value((Value::NationalStringLiteral("param2".to_string())).with_empty_span()), ], has_parentheses: false, + immediate: false, using: vec![], + into: vec![], + output: false, + default: false, }; assert_eq!( // Microsoft SQL Server does not use parentheses around arguments for EXECUTE @@ -10270,11 +11854,67 @@ 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] +fn parse_execute_immediate() { + let dialects = all_dialects_where(|d| d.supports_execute_immediate()); + + let expected = Statement::Execute { + parameters: vec![Expr::Value( + (Value::SingleQuotedString("SELECT 1".to_string())).with_empty_span(), + )], + immediate: true, + using: vec![ExprWithAlias { + expr: Expr::value(number("1")), + alias: Some(Ident::new("b")), + }], + 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"); + assert_eq!(expected, stmt); + + dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1' INTO a, b USING 1 AS x, y"); + dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1' USING 1 AS x, y"); + dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1' INTO a, b"); + dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1'"); + dialects.verified_stmt("EXECUTE 'SELECT 1'"); + + assert_eq!( + ParserError::ParserError("Expected: identifier, found: ,".to_string()), + dialects + .parse_sql_statements("EXECUTE IMMEDIATE 'SELECT 1' USING 1 AS, y") + .unwrap_err() + ); } #[test] fn parse_create_table_collate() { - pg_and_generic().verified_stmt("CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\")"); + all_dialects().verified_stmt("CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\")"); + // check ordering is preserved + all_dialects().verified_stmt( + "CREATE TABLE tbl (foo INT, bar TEXT CHARACTER SET utf8mb4 COLLATE \"de_DE\")", + ); + all_dialects().verified_stmt( + "CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\" CHARACTER SET utf8mb4)", + ); } #[test] @@ -10341,16 +11981,17 @@ fn parse_unload() { assert_eq!( unload, Statement::Unload { - query: Box::new(Query { + query: Some(Box::new(Query { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, 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(vec![Ident::new("tab")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("tab")])), joins: vec![], }], lateral_views: vec![], @@ -10366,18 +12007,18 @@ fn parse_unload() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), with: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, order_by: None, settings: None, format_clause: None, - }), + pipe_operators: vec![], + })), to: Ident { value: "s3://...".to_string(), quote_style: Some('\''), @@ -10389,10 +12030,123 @@ fn parse_unload() { quote_style: None, span: Span::empty(), }, - value: Expr::Value(Value::SingleQuotedString("AVRO".to_string())) - }] + value: Expr::Value( + (Value::SingleQuotedString("AVRO".to_string())).with_empty_span() + ) + }], + query_text: None, + auth: None, + options: vec![], } ); + + one_statement_parses_to( + concat!( + "UNLOAD('SELECT 1') ", + "TO 's3://...' ", + "IAM_ROLE 'arn:aws:iam::123456789:role/role1' ", + "FORMAT AS CSV ", + "FORMAT AS PARQUET ", + "FORMAT AS JSON ", + "MAXFILESIZE AS 10 MB ", + "ROWGROUPSIZE AS 10 MB ", + "PARALLEL ON ", + "PARALLEL OFF ", + "REGION AS 'us-east-1'" + ), + concat!( + "UNLOAD('SELECT 1') ", + "TO 's3://...' ", + "IAM_ROLE 'arn:aws:iam::123456789:role/role1' ", + "CSV ", + "PARQUET ", + "JSON ", + "MAXFILESIZE 10 MB ", + "ROWGROUPSIZE 10 MB ", + "PARALLEL TRUE ", + "PARALLEL FALSE ", + "REGION 'us-east-1'" + ), + ); + + verified_stmt(concat!( + "UNLOAD('SELECT 1') ", + "TO 's3://...' ", + "IAM_ROLE 'arn:aws:iam::123456789:role/role1' ", + "PARTITION BY (c1, c2, c3)", + )); + verified_stmt(concat!( + "UNLOAD('SELECT 1') ", + "TO 's3://...' ", + "IAM_ROLE 'arn:aws:iam::123456789:role/role1' ", + "PARTITION BY (c1, c2, c3) INCLUDE", + )); + + verified_stmt(concat!( + "UNLOAD('SELECT 1') ", + "TO 's3://...' ", + "IAM_ROLE 'arn:aws:iam::123456789:role/role1' ", + "PARTITION BY (c1, c2, c3) INCLUDE ", + "MANIFEST" + )); + verified_stmt(concat!( + "UNLOAD('SELECT 1') ", + "TO 's3://...' ", + "IAM_ROLE 'arn:aws:iam::123456789:role/role1' ", + "PARTITION BY (c1, c2, c3) INCLUDE ", + "MANIFEST VERBOSE" + )); + + verified_stmt(concat!( + "UNLOAD('SELECT 1') ", + "TO 's3://...' ", + "IAM_ROLE 'arn:aws:iam::123456789:role/role1' ", + "PARTITION BY (c1, c2, c3) INCLUDE ", + "MANIFEST VERBOSE ", + "HEADER ", + "FIXEDWIDTH 'col1:1,col2:2' ", + "ENCRYPTED" + )); + verified_stmt(concat!( + "UNLOAD('SELECT 1') ", + "TO 's3://...' ", + "IAM_ROLE 'arn:aws:iam::123456789:role/role1' ", + "PARTITION BY (c1, c2, c3) INCLUDE ", + "MANIFEST VERBOSE ", + "HEADER ", + "FIXEDWIDTH 'col1:1,col2:2' ", + "ENCRYPTED AUTO" + )); + + verified_stmt(concat!( + "UNLOAD('SELECT 1') ", + "TO 's3://...' ", + "IAM_ROLE 'arn:aws:iam::123456789:role/role1' ", + "PARTITION BY (c1, c2, c3) INCLUDE ", + "MANIFEST VERBOSE ", + "HEADER ", + "FIXEDWIDTH 'col1:1,col2:2' ", + "ENCRYPTED AUTO ", + "BZIP2 ", + "GZIP ", + "ZSTD ", + "ADDQUOTES ", + "NULL 'nil' ", + "ESCAPE ", + "ALLOWOVERWRITE ", + "CLEANPATH ", + "PARALLEL ", + "PARALLEL TRUE ", + "PARALLEL FALSE ", + "MAXFILESIZE 10 ", + "MAXFILESIZE 10 MB ", + "MAXFILESIZE 10 GB ", + "ROWGROUPSIZE 10 ", + "ROWGROUPSIZE 10 MB ", + "ROWGROUPSIZE 10 GB ", + "REGION 'us-east-1' ", + "EXTENSION 'ext1'" + )); } #[test] @@ -10445,14 +12199,15 @@ 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, vec![ColumnDef { name: Ident::new("id".to_string()), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: Comment("comment without equal".to_string()), @@ -10460,8 +12215,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!(), @@ -10498,12 +12255,12 @@ fn parse_map_access_expr() { AccessExpr::Subscript(Subscript::Index { index: Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Expr::Value(number("1")).into(), + expr: Expr::value(number("1")).into(), }, }), AccessExpr::Subscript(Subscript::Index { index: Expr::Function(Function { - name: ObjectName(vec![Ident::with_span( + name: ObjectName::from(vec![Ident::with_span( Span::new(Location::of(1, 11), Location::of(1, 22)), "safe_offset", )]), @@ -10511,7 +12268,7 @@ fn parse_map_access_expr() { args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - number("2"), + (number("2")).with_empty_span(), )))], clauses: vec![], }), @@ -10543,8 +12300,9 @@ 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(vec![Ident::new("employees")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), joins: vec![], }], into: None, @@ -10564,9 +12322,9 @@ fn parse_connect_by() { condition: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "president".to_owned(), - ))), + right: Box::new(Expr::Value( + Value::SingleQuotedString("president".to_owned()).with_empty_span(), + )), }, relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), @@ -10576,6 +12334,7 @@ fn parse_connect_by() { ))))), }], }), + flavor: SelectFlavor::Standard, }; let connect_by_1 = concat!( @@ -10623,8 +12382,9 @@ 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(vec![Ident::new("employees")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), joins: vec![], }], into: None, @@ -10633,7 +12393,7 @@ fn parse_connect_by() { selection: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("employee_id"))), op: BinaryOperator::NotEq, - right: Box::new(Expr::Value(number("42"))), + right: Box::new(Expr::value(number("42"))), }), group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -10648,9 +12408,9 @@ fn parse_connect_by() { condition: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "president".to_owned(), - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("president".to_owned(),)).with_empty_span() + )), }, relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), @@ -10660,6 +12420,7 @@ fn parse_connect_by() { ))))), }], }), + flavor: SelectFlavor::Standard, } ); @@ -10688,6 +12449,20 @@ fn parse_connect_by() { #[test] fn test_selective_aggregation() { + let testing_dialects = all_dialects_where(|d| d.supports_filter_during_aggregation()); + let expected_dialects: Vec> = vec![ + Box::new(PostgreSqlDialect {}), + Box::new(DatabricksDialect {}), + Box::new(HiveDialect {}), + Box::new(SQLiteDialect {}), + Box::new(DuckDbDialect {}), + Box::new(GenericDialect {}), + ]; + assert_eq!(testing_dialects.dialects.len(), expected_dialects.len()); + expected_dialects + .into_iter() + .for_each(|d| assert!(d.supports_filter_during_aggregation())); + let sql = concat!( "SELECT ", "ARRAY_AGG(name) FILTER (WHERE name IS NOT NULL), ", @@ -10695,12 +12470,10 @@ fn test_selective_aggregation() { "FROM region" ); assert_eq!( - all_dialects_where(|d| d.supports_filter_during_aggregation()) - .verified_only_select(sql) - .projection, + testing_dialects.verified_only_select(sql).projection, vec![ SelectItem::UnnamedExpr(Expr::Function(Function { - name: ObjectName(vec![Ident::new("ARRAY_AGG")]), + name: ObjectName::from(vec![Ident::new("ARRAY_AGG")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -10719,7 +12492,7 @@ fn test_selective_aggregation() { })), SelectItem::ExprWithAlias { expr: Expr::Function(Function { - name: ObjectName(vec![Ident::new("ARRAY_AGG")]), + name: ObjectName::from(vec![Ident::new("ARRAY_AGG")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -10732,7 +12505,9 @@ fn test_selective_aggregation() { filter: Some(Box::new(Expr::Like { negated: false, expr: Box::new(Expr::Identifier(Ident::new("name"))), - pattern: Box::new(Expr::Value(Value::SingleQuotedString("a%".to_owned()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("a%".to_owned())).with_empty_span() + )), escape_char: None, any: false, })), @@ -10773,13 +12548,51 @@ fn test_group_by_grouping_sets() { ); } +#[test] +fn test_xmltable() { + all_dialects() + .verified_only_select("SELECT * FROM XMLTABLE('/root' PASSING data COLUMNS element TEXT)"); + + // Minimal meaningful working example: returns a single row with a single column named y containing the value z + all_dialects().verified_only_select( + "SELECT y FROM XMLTABLE('/X' PASSING 'z' COLUMNS y TEXT)", + ); + + // Test using subqueries + all_dialects().verified_only_select("SELECT y FROM XMLTABLE((SELECT '/X') PASSING (SELECT CAST('z' AS xml)) COLUMNS y TEXT PATH (SELECT 'y'))"); + + // NOT NULL + all_dialects().verified_only_select( + "SELECT y FROM XMLTABLE('/X' PASSING '' COLUMNS y TEXT NOT NULL)", + ); + + all_dialects().verified_only_select("SELECT * FROM XMLTABLE('/root/row' PASSING xmldata COLUMNS id INT PATH '@id', name TEXT PATH 'name/text()', value FLOAT PATH 'value')"); + + all_dialects().verified_only_select("SELECT * FROM XMLTABLE('//ROWS/ROW' PASSING data COLUMNS row_num FOR ORDINALITY, id INT PATH '@id', name TEXT PATH 'NAME' DEFAULT 'unnamed')"); + + // Example from https://www.postgresql.org/docs/15/functions-xml.html#FUNCTIONS-XML-PROCESSING + all_dialects().verified_only_select( + "SELECT xmltable.* FROM xmldata, XMLTABLE('//ROWS/ROW' PASSING data COLUMNS id INT PATH '@id', ordinality FOR ORDINALITY, \"COUNTRY_NAME\" TEXT, country_id TEXT PATH 'COUNTRY_ID', size_sq_km FLOAT PATH 'SIZE[@unit = \"sq_km\"]', size_other TEXT PATH 'concat(SIZE[@unit!=\"sq_km\"], \" \", SIZE[@unit!=\"sq_km\"]/@unit)', premier_name TEXT PATH 'PREMIER_NAME' DEFAULT 'not specified')" + ); + + // Example from DB2 docs without explicit PASSING clause: https://www.ibm.com/docs/en/db2/12.1.0?topic=xquery-simple-column-name-passing-xmlexists-xmlquery-xmltable + all_dialects().verified_only_select( + "SELECT X.* FROM T1, XMLTABLE('$CUSTLIST/customers/customerinfo' COLUMNS \"Cid\" BIGINT PATH '@Cid', \"Info\" XML PATH 'document{.}', \"History\" XML PATH 'NULL') AS X" + ); + + // Example from PostgreSQL with XMLNAMESPACES + all_dialects().verified_only_select( + "SELECT xmltable.* FROM XMLTABLE(XMLNAMESPACES('http://example.com/myns' AS x, 'http://example.com/b' AS \"B\"), '/x:example/x:item' PASSING (SELECT data FROM xmldata) COLUMNS foo INT PATH '@foo', bar INT PATH '@B:bar')" + ); +} + #[test] fn test_match_recognize() { use MatchRecognizePattern::*; use MatchRecognizeSymbol::*; use RepetitionQuantifier::*; - let table = table_from_name(ObjectName(vec![Ident::new("my_table")])); + let table = table_from_name(ObjectName::from(vec![Ident::new("my_table")])); fn check(options: &str, expect: TableFactor) { let select = all_dialects_where(|d| d.supports_match_recognize()).verified_only_select( @@ -10808,8 +12621,10 @@ fn test_match_recognize() { partition_by: vec![Expr::Identifier(Ident::new("company"))], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("price_date")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }], measures: vec![ @@ -11088,7 +12903,9 @@ fn test_select_wildcard_with_replace() { let expected = SelectItem::Wildcard(WildcardAdditionalOptions { opt_replace: Some(ReplaceSelectItem { items: vec![Box::new(ReplaceSelectElement { - expr: Expr::Value(Value::SingleQuotedString("widget".to_owned())), + expr: Expr::Value( + (Value::SingleQuotedString("widget".to_owned())).with_empty_span(), + ), column_name: Ident::new("item_name"), as_keyword: true, })], @@ -11107,13 +12924,13 @@ fn test_select_wildcard_with_replace() { expr: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("quantity"))), op: BinaryOperator::Divide, - right: Box::new(Expr::Value(number("2"))), + right: Box::new(Expr::value(number("2"))), }, column_name: Ident::new("quantity"), as_keyword: true, }), Box::new(ReplaceSelectElement { - expr: Expr::Value(number("3")), + expr: Expr::value(number("3")), column_name: Ident::new("order_id"), as_keyword: true, }), @@ -11163,7 +12980,7 @@ fn parse_odbc_scalar_function() { else { unreachable!("expected function") }; - assert_eq!(name, &ObjectName(vec![Ident::new("my_func")])); + assert_eq!(name, &ObjectName::from(vec![Ident::new("my_func")])); assert!(uses_odbc_syntax); matches!(args, FunctionArguments::List(l) if l.args.len() == 2); @@ -11189,20 +13006,22 @@ fn test_dictionary_syntax() { ); } + check("{}", Expr::Dictionary(vec![])); + check( "{'Alberta': 'Edmonton', 'Manitoba': 'Winnipeg'}", Expr::Dictionary(vec![ DictionaryField { key: Ident::with_quote('\'', "Alberta"), - value: Box::new(Expr::Value(Value::SingleQuotedString( - "Edmonton".to_owned(), - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("Edmonton".to_owned())).with_empty_span(), + )), }, DictionaryField { key: Ident::with_quote('\'', "Manitoba"), - value: Box::new(Expr::Value(Value::SingleQuotedString( - "Winnipeg".to_owned(), - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("Winnipeg".to_owned())).with_empty_span(), + )), }, ]), ); @@ -11214,9 +13033,9 @@ fn test_dictionary_syntax() { key: Ident::with_quote('\'', "start"), value: Box::new(Expr::Cast { kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2023-04-01".to_owned(), - ))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2023-04-01".to_owned())).with_empty_span(), + )), data_type: DataType::Timestamp(None, TimezoneInfo::None), format: None, }), @@ -11225,9 +13044,9 @@ fn test_dictionary_syntax() { key: Ident::with_quote('\'', "end"), value: Box::new(Expr::Cast { kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2023-04-05".to_owned(), - ))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2023-04-05".to_owned())).with_empty_span(), + )), data_type: DataType::Timestamp(None, TimezoneInfo::None), format: None, }), @@ -11250,25 +13069,27 @@ fn test_map_syntax() { Expr::Map(Map { entries: vec![ MapEntry { - key: Box::new(Expr::Value(Value::SingleQuotedString("Alberta".to_owned()))), - value: Box::new(Expr::Value(Value::SingleQuotedString( - "Edmonton".to_owned(), - ))), + key: Box::new(Expr::Value( + (Value::SingleQuotedString("Alberta".to_owned())).with_empty_span(), + )), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("Edmonton".to_owned())).with_empty_span(), + )), }, MapEntry { - key: Box::new(Expr::Value(Value::SingleQuotedString( - "Manitoba".to_owned(), - ))), - value: Box::new(Expr::Value(Value::SingleQuotedString( - "Winnipeg".to_owned(), - ))), + key: Box::new(Expr::Value( + (Value::SingleQuotedString("Manitoba".to_owned())).with_empty_span(), + )), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("Winnipeg".to_owned())).with_empty_span(), + )), }, ], }), ); fn number_expr(s: &str) -> Expr { - Expr::Value(number(s)) + Expr::value(number(s)) } check( @@ -11296,14 +13117,14 @@ fn test_map_syntax() { elem: vec![number_expr("1"), number_expr("2"), number_expr("3")], named: false, })), - value: Box::new(Expr::Value(number("10.0"))), + value: Box::new(Expr::value(number("10.0"))), }, MapEntry { key: Box::new(Expr::Array(Array { elem: vec![number_expr("4"), number_expr("5"), number_expr("6")], named: false, })), - value: Box::new(Expr::Value(number("20.0"))), + value: Box::new(Expr::value(number("20.0"))), }, ], }), @@ -11315,17 +13136,21 @@ fn test_map_syntax() { root: Box::new(Expr::Map(Map { entries: vec![ MapEntry { - key: Box::new(Expr::Value(Value::SingleQuotedString("a".to_owned()))), + key: Box::new(Expr::Value( + (Value::SingleQuotedString("a".to_owned())).with_empty_span(), + )), value: Box::new(number_expr("10")), }, MapEntry { - key: Box::new(Expr::Value(Value::SingleQuotedString("b".to_owned()))), + key: Box::new(Expr::Value( + (Value::SingleQuotedString("b".to_owned())).with_empty_span(), + )), value: Box::new(number_expr("20")), }, ], })), access_chain: vec![AccessExpr::Subscript(Subscript::Index { - index: Expr::Value(Value::SingleQuotedString("a".to_owned())), + index: Expr::Value((Value::SingleQuotedString("a".to_owned())).with_empty_span()), })], }, ); @@ -11420,21 +13245,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()) @@ -11474,10 +13284,13 @@ fn test_extract_seconds_ok() { syntax: ExtractSyntax::From, expr: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2 seconds".to_string() - ))), - data_type: DataType::Interval, + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2 seconds".to_string())).with_empty_span() + )), + data_type: DataType::Interval { + fields: None, + precision: None + }, format: None, }), } @@ -11499,13 +13312,17 @@ fn test_extract_seconds_ok() { syntax: ExtractSyntax::From, expr: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2 seconds".to_string(), - ))), - data_type: DataType::Interval, + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2 seconds".to_string())).with_empty_span(), + )), + data_type: DataType::Interval { + fields: None, + precision: None, + }, format: None, }), })], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -11521,16 +13338,16 @@ fn test_extract_seconds_ok() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }))]; assert_eq!(actual_ast, expected_ast); @@ -11552,10 +13369,13 @@ fn test_extract_seconds_single_quote_ok() { syntax: ExtractSyntax::From, expr: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2 seconds".to_string() - ))), - data_type: DataType::Interval, + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2 seconds".to_string())).with_empty_span() + )), + data_type: DataType::Interval { + fields: None, + precision: None + }, format: None, }), } @@ -11577,8 +13397,8 @@ fn test_extract_seconds_single_quote_err() { fn test_truncate_table_with_on_cluster() { let sql = "TRUNCATE TABLE t ON CLUSTER cluster_name"; match all_dialects().verified_stmt(sql) { - Statement::Truncate { on_cluster, .. } => { - assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); + Statement::Truncate(truncate) => { + assert_eq!(truncate.on_cluster, Some(Ident::new("cluster_name"))); } _ => panic!("Expected: TRUNCATE TABLE statement"), } @@ -11605,11 +13425,11 @@ fn parse_explain_with_option_list() { Some(vec![ UtilityOption { name: Ident::new("ANALYZE"), - arg: Some(Expr::Value(Value::Boolean(false))), + arg: Some(Expr::Value((Value::Boolean(false)).with_empty_span())), }, UtilityOption { name: Ident::new("VERBOSE"), - arg: Some(Expr::Value(Value::Boolean(true))), + arg: Some(Expr::Value((Value::Boolean(true)).with_empty_span())), }, ]), ); @@ -11645,7 +13465,9 @@ fn parse_explain_with_option_list() { }, UtilityOption { name: Ident::new("FORMAT2"), - arg: Some(Expr::Value(Value::SingleQuotedString("JSON".to_string()))), + arg: Some(Expr::Value( + (Value::SingleQuotedString("JSON".to_string())).with_empty_span(), + )), }, UtilityOption { name: Ident::new("FORMAT3"), @@ -11667,20 +13489,26 @@ fn parse_explain_with_option_list() { Some(vec![ UtilityOption { name: Ident::new("NUM1"), - arg: Some(Expr::Value(Value::Number("10".parse().unwrap(), false))), + arg: Some(Expr::Value( + (Value::Number("10".parse().unwrap(), false)).with_empty_span(), + )), }, UtilityOption { name: Ident::new("NUM2"), arg: Some(Expr::UnaryOp { op: UnaryOperator::Plus, - expr: Box::new(Expr::Value(Value::Number("10.1".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Value::Number("10.1".parse().unwrap(), false)).with_empty_span(), + )), }), }, UtilityOption { name: Ident::new("NUM3"), arg: Some(Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(Value::Number("10.2".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Value::Number("10.2".parse().unwrap(), false)).with_empty_span(), + )), }), }, ]), @@ -11693,7 +13521,7 @@ fn parse_explain_with_option_list() { }, UtilityOption { name: Ident::new("VERBOSE"), - arg: Some(Expr::Value(Value::Boolean(true))), + arg: Some(Expr::Value((Value::Boolean(true)).with_empty_span())), }, UtilityOption { name: Ident::new("WAL"), @@ -11707,7 +13535,9 @@ fn parse_explain_with_option_list() { name: Ident::new("USER_DEF_NUM"), arg: Some(Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(Value::Number("100.1".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Value::Number("100.1".parse().unwrap(), false)).with_empty_span(), + )), }), }, ]; @@ -11752,15 +13582,21 @@ fn test_create_policy() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("c0"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), }) ); assert_eq!( with_check, Some(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + left: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), }) ); } @@ -11950,6 +13786,183 @@ fn test_alter_policy() { ); } +#[test] +fn test_create_connector() { + let sql = "CREATE CONNECTOR my_connector \ + TYPE 'jdbc' \ + URL 'jdbc:mysql://localhost:3306/mydb' \ + WITH DCPROPERTIES('user' = 'root', 'password' = 'password')"; + let dialects = all_dialects(); + match dialects.verified_stmt(sql) { + Statement::CreateConnector(CreateConnector { + name, + connector_type, + url, + with_dcproperties, + .. + }) => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!(connector_type, Some("jdbc".to_string())); + assert_eq!(url, Some("jdbc:mysql://localhost:3306/mydb".to_string())); + assert_eq!( + with_dcproperties, + Some(vec![ + SqlOption::KeyValue { + key: Ident::with_quote('\'', "user"), + value: Expr::Value( + (Value::SingleQuotedString("root".to_string())).with_empty_span() + ) + }, + SqlOption::KeyValue { + key: Ident::with_quote('\'', "password"), + value: Expr::Value( + (Value::SingleQuotedString("password".to_string())).with_empty_span() + ) + } + ]) + ); + } + _ => unreachable!(), + } + + // omit IF NOT EXISTS/TYPE/URL/COMMENT/WITH DCPROPERTIES clauses is allowed + dialects.verified_stmt("CREATE CONNECTOR my_connector"); + + // missing connector name + assert_eq!( + dialects + .parse_sql_statements("CREATE CONNECTOR") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: EOF" + ); +} + +#[test] +fn test_drop_connector() { + let dialects = all_dialects(); + match dialects.verified_stmt("DROP CONNECTOR IF EXISTS my_connector") { + Statement::DropConnector { if_exists, name } => { + assert_eq!(if_exists, true); + assert_eq!(name.to_string(), "my_connector"); + } + _ => unreachable!(), + } + + // omit IF EXISTS is allowed + dialects.verified_stmt("DROP CONNECTOR my_connector"); + + // missing connector name + assert_eq!( + dialects + .parse_sql_statements("DROP CONNECTOR") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: EOF" + ); +} + +#[test] +fn test_alter_connector() { + let dialects = all_dialects(); + match dialects.verified_stmt( + "ALTER CONNECTOR my_connector SET DCPROPERTIES('user' = 'root', 'password' = 'password')", + ) { + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!( + properties, + Some(vec![ + SqlOption::KeyValue { + key: Ident::with_quote('\'', "user"), + value: Expr::Value( + (Value::SingleQuotedString("root".to_string())).with_empty_span() + ) + }, + SqlOption::KeyValue { + key: Ident::with_quote('\'', "password"), + value: Expr::Value( + (Value::SingleQuotedString("password".to_string())).with_empty_span() + ) + } + ]) + ); + assert_eq!(url, None); + assert_eq!(owner, None); + } + _ => unreachable!(), + } + + match dialects + .verified_stmt("ALTER CONNECTOR my_connector SET URL 'jdbc:mysql://localhost:3306/mydb'") + { + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!(properties, None); + assert_eq!(url, Some("jdbc:mysql://localhost:3306/mydb".to_string())); + assert_eq!(owner, None); + } + _ => unreachable!(), + } + + match dialects.verified_stmt("ALTER CONNECTOR my_connector SET OWNER USER 'root'") { + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!(properties, None); + assert_eq!(url, None); + assert_eq!( + owner, + Some(AlterConnectorOwner::User(Ident::with_quote('\'', "root"))) + ); + } + _ => unreachable!(), + } + + match dialects.verified_stmt("ALTER CONNECTOR my_connector SET OWNER ROLE 'admin'") { + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!(properties, None); + assert_eq!(url, None); + assert_eq!( + owner, + Some(AlterConnectorOwner::Role(Ident::with_quote('\'', "admin"))) + ); + } + _ => unreachable!(), + } + + // Wrong option name + assert_eq!( + dialects + .parse_sql_statements( + "ALTER CONNECTOR my_connector SET WRONG 'jdbc:mysql://localhost:3306/mydb'" + ) + .unwrap_err() + .to_string(), + "sql parser error: Expected: end of statement, found: WRONG" + ); +} + #[test] fn test_select_where_with_like_or_ilike_any() { verified_stmt(r#"SELECT * FROM x WHERE a ILIKE ANY '%abc%'"#); @@ -11996,68 +14009,76 @@ fn test_try_convert() { #[test] fn parse_method_select() { - let dialects = all_dialects_where(|d| d.supports_methods()); - let _ = dialects.verified_only_select( + let _ = verified_only_select( "SELECT LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T", ); - let _ = dialects.verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"); - let _ = dialects - .verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T"); + let _ = verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"); + let _ = verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T"); // `CONVERT` support - let dialects = all_dialects_where(|d| { - d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() - }); + let dialects = + all_dialects_where(|d| d.supports_try_convert() && d.convert_type_before_value()); let _ = dialects.verified_only_select("SELECT CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T"); } #[test] fn parse_method_expr() { - let dialects = all_dialects_where(|d| d.supports_methods()); - let expr = dialects - .verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')"); + let expr = + verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')"); match expr { - Expr::Method(Method { expr, method_chain }) => { - assert!(matches!(*expr, Expr::Function(_))); + Expr::CompoundFieldAccess { root, access_chain } => { + assert!(matches!(*root, Expr::Function(_))); assert!(matches!( - method_chain[..], - [Function { .. }, Function { .. }] + access_chain[..], + [ + AccessExpr::Dot(Expr::Function(_)), + AccessExpr::Dot(Expr::Function(_)) + ] )); } _ => unreachable!(), } - let expr = dialects.verified_expr( + + let expr = verified_expr( "(SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')", ); match expr { - Expr::Method(Method { expr, method_chain }) => { - assert!(matches!(*expr, Expr::Subquery(_))); - assert!(matches!(method_chain[..], [Function { .. }])); + Expr::CompoundFieldAccess { root, access_chain } => { + assert!(matches!(*root, Expr::Subquery(_))); + assert!(matches!( + access_chain[..], + [AccessExpr::Dot(Expr::Function(_))] + )); } _ => unreachable!(), } - let expr = dialects.verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')"); + let expr = verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')"); match expr { - Expr::Method(Method { expr, method_chain }) => { - assert!(matches!(*expr, Expr::Cast { .. })); - assert!(matches!(method_chain[..], [Function { .. }])); + Expr::CompoundFieldAccess { root, access_chain } => { + assert!(matches!(*root, Expr::Cast { .. })); + assert!(matches!( + access_chain[..], + [AccessExpr::Dot(Expr::Function(_))] + )); } _ => unreachable!(), } // `CONVERT` support - let dialects = all_dialects_where(|d| { - d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() - }); + let dialects = + all_dialects_where(|d| d.supports_try_convert() && d.convert_type_before_value()); let expr = dialects.verified_expr( "CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')", ); match expr { - Expr::Method(Method { expr, method_chain }) => { - assert!(matches!(*expr, Expr::Convert { .. })); + Expr::CompoundFieldAccess { root, access_chain } => { + assert!(matches!(*root, Expr::Convert { .. })); assert!(matches!( - method_chain[..], - [Function { .. }, Function { .. }] + access_chain[..], + [ + AccessExpr::Dot(Expr::Function(_)), + AccessExpr::Dot(Expr::Function(_)) + ] )); } _ => unreachable!(), @@ -12230,7 +14251,7 @@ fn parse_load_data() { assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(false, overwrite); assert_eq!( - ObjectName(vec![Ident::new("test"), Ident::new("my_table")]), + ObjectName::from(vec![Ident::new("test"), Ident::new("my_table")]), table_name ); assert_eq!(None, partitioned); @@ -12253,7 +14274,7 @@ fn parse_load_data() { assert_eq!(false, local); assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(true, overwrite); - assert_eq!(ObjectName(vec![Ident::new("my_table")]), table_name); + assert_eq!(ObjectName::from(vec![Ident::new("my_table")]), table_name); assert_eq!(None, partitioned); assert_eq!(None, table_format); } @@ -12290,7 +14311,7 @@ fn parse_load_data() { assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(false, overwrite); assert_eq!( - ObjectName(vec![Ident::new("test"), Ident::new("my_table")]), + ObjectName::from(vec![Ident::new("test"), Ident::new("my_table")]), table_name ); assert_eq!(None, partitioned); @@ -12328,18 +14349,22 @@ fn parse_load_data() { assert_eq!(true, local); assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(false, overwrite); - assert_eq!(ObjectName(vec![Ident::new("my_table")]), table_name); + assert_eq!(ObjectName::from(vec![Ident::new("my_table")]), table_name); assert_eq!( Some(vec![ Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("year"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("2024".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("2024".parse().unwrap(), false)).with_empty_span() + )), }, Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("month"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("11".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("11".parse().unwrap(), false)).with_empty_span() + )), } ]), partitioned @@ -12364,7 +14389,7 @@ fn parse_load_data() { assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(true, overwrite); assert_eq!( - ObjectName(vec![Ident::new("good"), Ident::new("my_table")]), + ObjectName::from(vec![Ident::new("good"), Ident::new("my_table")]), table_name ); assert_eq!( @@ -12372,24 +14397,34 @@ fn parse_load_data() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("year"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("2024".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("2024".parse().unwrap(), false)).with_empty_span() + )), }, Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("month"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("11".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("11".parse().unwrap(), false)).with_empty_span() + )), } ]), partitioned ); assert_eq!( Some(HiveLoadDataFormat { - serde: Expr::Value(Value::SingleQuotedString( - "org.apache.hadoop.hive.serde2.OpenCSVSerde".to_string() - )), - input_format: Expr::Value(Value::SingleQuotedString( - "org.apache.hadoop.mapred.TextInputFormat".to_string() - )) + serde: Expr::Value( + (Value::SingleQuotedString( + "org.apache.hadoop.hive.serde2.OpenCSVSerde".to_string() + )) + .with_empty_span() + ), + input_format: Expr::Value( + (Value::SingleQuotedString( + "org.apache.hadoop.mapred.TextInputFormat".to_string() + )) + .with_empty_span() + ) }), table_format ); @@ -12467,7 +14502,9 @@ fn parse_bang_not() { Box::new(Expr::Nested(Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("b"))), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(Value::Number("3".parse().unwrap(), false))), + right: Box::new(Expr::Value( + Value::Number("3".parse().unwrap(), false).with_empty_span(), + )), }))), ] .into_iter() @@ -12718,7 +14755,7 @@ fn parse_composite_access_expr() { verified_expr("f(a).b"), Expr::CompoundFieldAccess { root: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("f")]), + name: ObjectName::from(vec![Ident::new("f")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -12742,7 +14779,7 @@ fn parse_composite_access_expr() { verified_expr("f(a).b.c"), Expr::CompoundFieldAccess { root: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("f")]), + name: ObjectName::from(vec![Ident::new("f")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -12768,7 +14805,7 @@ fn parse_composite_access_expr() { let stmt = verified_only_select("SELECT f(a).b FROM t WHERE f(a).b IS NOT NULL"); let expr = Expr::CompoundFieldAccess { root: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("f")]), + name: ObjectName::from(vec![Ident::new("f")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -12809,11 +14846,13 @@ fn parse_composite_access_expr() { values: vec![ Expr::Named { name: Ident::new("a"), - expr: Box::new(Expr::Value(Number("1".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Number("1".parse().unwrap(), false)).with_empty_span(), + )), }, Expr::Named { name: Ident::new("b"), - expr: Box::new(Expr::Value(Value::Null)), + expr: Box::new(Expr::Value((Value::Null).with_empty_span())), }, ], fields: vec![], @@ -12821,7 +14860,7 @@ fn parse_composite_access_expr() { }, Expr::Named { name: Ident::new("d"), - expr: Box::new(Expr::Value(Value::Null)), + expr: Box::new(Expr::Value((Value::Null).with_empty_span())), }, ], fields: vec![], @@ -12848,16 +14887,19 @@ fn parse_create_table_with_enum_types() { vec![ EnumMember::NamedValue( "a".to_string(), - Expr::Value(Number("1".parse().unwrap(), false)) + Expr::Value( + (Number("1".parse().unwrap(), false)).with_empty_span() + ) ), EnumMember::NamedValue( "b".to_string(), - Expr::Value(Number("2".parse().unwrap(), false)) + Expr::Value( + (Number("2".parse().unwrap(), false)).with_empty_span() + ) ) ], Some(8) ), - collation: None, options: vec![], }, ColumnDef { @@ -12866,16 +14908,19 @@ fn parse_create_table_with_enum_types() { vec![ EnumMember::NamedValue( "a".to_string(), - Expr::Value(Number("1".parse().unwrap(), false)) + Expr::Value( + (Number("1".parse().unwrap(), false)).with_empty_span() + ) ), EnumMember::NamedValue( "b".to_string(), - Expr::Value(Number("2".parse().unwrap(), false)) + Expr::Value( + (Number("2".parse().unwrap(), false)).with_empty_span() + ) ) ], Some(16) ), - collation: None, options: vec![], }, ColumnDef { @@ -12887,7 +14932,6 @@ fn parse_create_table_with_enum_types() { ], None ), - collation: None, options: vec![], } ], @@ -12929,11 +14973,10 @@ fn test_table_sample() { #[test] fn overflow() { - let expr = std::iter::repeat("1") - .take(1000) + 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(); @@ -12947,8 +14990,8 @@ fn parse_select_without_projection() { #[test] fn parse_update_from_before_select() { - all_dialects() - .verified_stmt("UPDATE t1 FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 SET name = t2.name WHERE t1.id = t2.id"); + verified_stmt("UPDATE t1 FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 SET name = t2.name WHERE t1.id = t2.id"); + verified_stmt("UPDATE t1 FROM U, (SELECT id FROM V) AS W SET a = b WHERE 1 = 1"); let query = "UPDATE t1 FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 SET name = t2.name FROM (SELECT name from t2) AS t2"; @@ -12962,6 +15005,33 @@ fn parse_overlaps() { verified_stmt("SELECT (DATE '2016-01-10', DATE '2016-02-01') OVERLAPS (DATE '2016-01-20', DATE '2016-02-10')"); } +#[test] +fn parse_column_definition_trailing_commas() { + let dialects = all_dialects_where(|d| d.supports_column_definition_trailing_commas()); + + dialects.one_statement_parses_to("CREATE TABLE T (x INT64,)", "CREATE TABLE T (x INT64)"); + dialects.one_statement_parses_to( + "CREATE TABLE T (x INT64, y INT64, )", + "CREATE TABLE T (x INT64, y INT64)", + ); + dialects.one_statement_parses_to( + "CREATE VIEW T (x, y, ) AS SELECT 1", + "CREATE VIEW T (x, y) AS SELECT 1", + ); + + let unsupported_dialects = all_dialects_where(|d| { + !d.supports_projection_trailing_commas() && !d.supports_trailing_commas() + }); + assert_eq!( + unsupported_dialects + .parse_sql_statements("CREATE TABLE employees (name text, age int,)") + .unwrap_err(), + ParserError::ParserError( + "Expected: column name or constraint definition, found: )".to_string() + ), + ); +} + #[test] fn test_trailing_commas_in_from() { let dialects = all_dialects_where(|d| d.supports_from_trailing_commas()); @@ -12982,7 +15052,7 @@ fn test_trailing_commas_in_from() { let sql = "SELECT a FROM b, WHERE c = 1"; let _ = dialects.parse_sql_statements(sql).unwrap(); - // nasted + // nested let sql = "SELECT 1, 2 FROM (SELECT * FROM t,),"; let _ = dialects.parse_sql_statements(sql).unwrap(); @@ -12992,3 +15062,2638 @@ fn test_trailing_commas_in_from() { "SELECT 1, 2 FROM (SELECT * FROM t1), (SELECT * FROM t2)", ); } + +#[test] +#[cfg(feature = "visitor")] +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![]; + let _ = sqlparser::ast::visit_expressions(&stmt, |expr| { + visited.push(expr.to_string()); + core::ops::ControlFlow::<()>::Continue(()) + }); + + assert_eq!( + visited, + [ + "CASE a WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE 5 END", + "a", + "1", + "2", + "3", + "4", + "5" + ] + ); +} + +#[test] +fn parse_case_statement() { + let sql = "CASE 1 WHEN 2 THEN SELECT 1; SELECT 2; ELSE SELECT 3; END CASE"; + let Statement::Case(stmt) = verified_stmt(sql) else { + unreachable!() + }; + + assert_eq!(Some(Expr::value(number("1"))), stmt.match_expr); + assert_eq!( + Some(Expr::value(number("2"))), + stmt.when_blocks[0].condition + ); + assert_eq!(2, stmt.when_blocks[0].statements().len()); + assert_eq!(1, stmt.else_block.unwrap().statements().len()); + + verified_stmt(concat!( + "CASE 1", + " WHEN a THEN", + " SELECT 1; SELECT 2; SELECT 3;", + " WHEN b THEN", + " SELECT 4; SELECT 5;", + " ELSE", + " SELECT 7; SELECT 8;", + " END CASE" + )); + verified_stmt(concat!( + "CASE 1", + " WHEN a THEN", + " SELECT 1; SELECT 2; SELECT 3;", + " WHEN b THEN", + " SELECT 4; SELECT 5;", + " END CASE" + )); + verified_stmt(concat!( + "CASE 1", + " WHEN a THEN", + " SELECT 1; SELECT 2; SELECT 3;", + " END CASE" + )); + verified_stmt(concat!( + "CASE 1", + " WHEN a THEN", + " SELECT 1; SELECT 2; SELECT 3;", + " END" + )); + + assert_eq!( + ParserError::ParserError("Expected: THEN, found: END".to_string()), + parse_sql_statements("CASE 1 WHEN a END").unwrap_err() + ); + assert_eq!( + ParserError::ParserError("Expected: WHEN, found: ELSE".to_string()), + parse_sql_statements("CASE 1 ELSE SELECT 1; END").unwrap_err() + ); +} + +#[test] +fn test_case_statement_span() { + let sql = "CASE 1 WHEN 2 THEN SELECT 1; SELECT 2; ELSE SELECT 3; END CASE"; + let mut parser = Parser::new(&GenericDialect {}).try_with_sql(sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new(Location::new(1, 1), Location::new(1, sql.len() as u64 + 1)) + ); +} + +#[test] +fn parse_if_statement() { + let dialects = all_dialects_except(|d| d.is::()); + + let sql = "IF 1 THEN SELECT 1; ELSEIF 2 THEN SELECT 2; ELSE SELECT 3; END IF"; + let Statement::If(IfStatement { + if_block, + elseif_blocks, + else_block, + .. + }) = dialects.verified_stmt(sql) + else { + unreachable!() + }; + assert_eq!(Some(Expr::value(number("1"))), if_block.condition); + assert_eq!(Some(Expr::value(number("2"))), elseif_blocks[0].condition); + assert_eq!(1, else_block.unwrap().statements().len()); + + dialects.verified_stmt(concat!( + "IF 1 THEN", + " SELECT 1;", + " SELECT 2;", + " SELECT 3;", + " ELSEIF 2 THEN", + " SELECT 4;", + " SELECT 5;", + " ELSEIF 3 THEN", + " SELECT 6;", + " SELECT 7;", + " ELSE", + " SELECT 8;", + " SELECT 9;", + " END IF" + )); + dialects.verified_stmt(concat!( + "IF 1 THEN", + " SELECT 1;", + " SELECT 2;", + " ELSE", + " SELECT 3;", + " SELECT 4;", + " END IF" + )); + dialects.verified_stmt(concat!( + "IF 1 THEN", + " SELECT 1;", + " SELECT 2;", + " SELECT 3;", + " ELSEIF 2 THEN", + " SELECT 3;", + " SELECT 4;", + " END IF" + )); + dialects.verified_stmt(concat!("IF 1 THEN", " SELECT 1;", " SELECT 2;", " END IF")); + dialects.verified_stmt(concat!( + "IF (1) THEN", + " SELECT 1;", + " SELECT 2;", + " END IF" + )); + dialects.verified_stmt("IF 1 THEN END IF"); + dialects.verified_stmt("IF 1 THEN SELECT 1; ELSEIF 1 THEN END IF"); + + assert_eq!( + ParserError::ParserError("Expected: IF, found: EOF".to_string()), + dialects + .parse_sql_statements("IF 1 THEN SELECT 1; ELSEIF 1 THEN SELECT 2; END") + .unwrap_err() + ); +} + +#[test] +fn test_if_statement_span() { + let sql = "IF 1=1 THEN SELECT 1; ELSEIF 1=2 THEN SELECT 2; ELSE SELECT 3; END IF"; + let mut parser = Parser::new(&GenericDialect {}).try_with_sql(sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new(Location::new(1, 1), Location::new(1, sql.len() as u64 + 1)) + ); +} + +#[test] +fn test_if_statement_multiline_span() { + let sql_line1 = "IF 1 = 1 THEN SELECT 1;"; + let sql_line2 = "ELSEIF 1 = 2 THEN SELECT 2;"; + let sql_line3 = "ELSE SELECT 3;"; + let sql_line4 = "END IF"; + let sql = [sql_line1, sql_line2, sql_line3, sql_line4].join("\n"); + let mut parser = Parser::new(&GenericDialect {}).try_with_sql(&sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new( + Location::new(1, 1), + Location::new(4, sql_line4.len() as u64 + 1) + ) + ); +} + +#[test] +fn test_conditional_statement_span() { + let sql = "IF 1=1 THEN SELECT 1; ELSEIF 1=2 THEN SELECT 2; ELSE SELECT 3; END IF"; + let mut parser = Parser::new(&GenericDialect {}).try_with_sql(sql).unwrap(); + match parser.parse_statement().unwrap() { + Statement::If(IfStatement { + if_block, + elseif_blocks, + else_block, + .. + }) => { + assert_eq!( + Span::new(Location::new(1, 1), Location::new(1, 21)), + if_block.span() + ); + assert_eq!( + Span::new(Location::new(1, 23), Location::new(1, 47)), + elseif_blocks[0].span() + ); + assert_eq!( + Span::new(Location::new(1, 49), Location::new(1, 62)), + else_block.unwrap().span() + ); + } + stmt => panic!("Unexpected statement: {stmt:?}"), + } +} + +#[test] +fn parse_raise_statement() { + let sql = "RAISE USING MESSAGE = 42"; + let Statement::Raise(stmt) = verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + Some(RaiseStatementValue::UsingMessage(Expr::value(number("42")))), + stmt.value + ); + + verified_stmt("RAISE USING MESSAGE = 'error'"); + verified_stmt("RAISE myerror"); + verified_stmt("RAISE 42"); + verified_stmt("RAISE using"); + verified_stmt("RAISE"); + + assert_eq!( + ParserError::ParserError("Expected: =, found: error".to_string()), + parse_sql_statements("RAISE USING MESSAGE error").unwrap_err() + ); +} + +#[test] +fn test_lambdas() { + let dialects = all_dialects_where(|d| d.supports_lambda_functions()); + + #[rustfmt::skip] + let sql = concat!( + "SELECT array_sort(array('Hello', 'World'), ", + "(p1, p2) -> CASE WHEN p1 = p2 THEN 0 ", + "WHEN reverse(p1) < reverse(p2) THEN -1 ", + "ELSE 1 END)", + ); + pretty_assertions::assert_eq!( + SelectItem::UnnamedExpr(call( + "array_sort", + [ + call( + "array", + [ + Expr::Value( + (Value::SingleQuotedString("Hello".to_owned())).with_empty_span() + ), + Expr::Value( + (Value::SingleQuotedString("World".to_owned())).with_empty_span() + ) + ] + ), + 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 { + condition: Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("p1"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("p2"))) + }, + result: Expr::value(number("0")), + }, + CaseWhen { + condition: Expr::BinaryOp { + left: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p1"))] + )), + op: BinaryOperator::Lt, + right: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p2"))] + )), + }, + result: Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::value(number("1"))) + } + }, + ], + else_result: Some(Box::new(Expr::value(number("1")))), + }) + }) + ] + )), + dialects.verified_only_select(sql).projection[0] + ); + + dialects.verified_expr( + "map_zip_with(map(1, 'a', 2, 'b'), map(1, 'x', 2, 'y'), (k, v1, v2) -> concat(v1, v2))", + ); + dialects.verified_expr("transform(array(1, 2, 3), x -> x + 1)"); +} + +#[test] +fn test_select_from_first() { + let dialects = all_dialects_where(|d| d.supports_from_first_select()); + let q1 = "FROM capitals"; + let q2 = "FROM capitals SELECT *"; + + for (q, flavor, projection) in [ + (q1, SelectFlavor::FromFirstNoSelect, vec![]), + ( + q2, + SelectFlavor::FromFirst, + vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + ), + ] { + let ast = dialects.verified_query(q); + let expected = Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), + distinct: None, + top: None, + projection, + exclude: None, + top_before_distinct: false, + into: None, + from: vec![TableWithJoins { + relation: table_from_name(ObjectName::from(vec![Ident { + value: "capitals".to_string(), + quote_style: None, + span: Span::empty(), + }])), + 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![], + window_before_qualify: false, + qualify: None, + value_table_mode: None, + connect_by: None, + flavor, + }))), + order_by: None, + limit_clause: None, + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + pipe_operators: vec![], + }; + assert_eq!(expected, ast); + assert_eq!(ast.to_string(), q); + } +} + +#[test] +fn test_geometric_unary_operators() { + // Number of points in path or polygon + let sql = "# path '((1,0),(0,1),(-1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::Hash, + .. + } + )); + + // Length or circumference + let sql = "@-@ path '((0,0),(1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::AtDashAt, + .. + } + )); + + // Center + let sql = "@@ circle '((0,0),10)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::DoubleAt, + .. + } + )); + // Is horizontal? + let sql = "?- lseg '((-1,0),(1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::QuestionDash, + .. + } + )); + + // Is vertical? + let sql = "?| lseg '((-1,0),(1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::QuestionPipe, + .. + } + )); +} + +#[test] +fn test_geometry_type() { + let sql = "point '1,2'"; + assert_eq!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::TypedString(TypedString { + data_type: DataType::GeometricType(GeometricTypeKind::Point), + 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(TypedString { + data_type: DataType::GeometricType(GeometricTypeKind::Line), + 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(TypedString { + data_type: DataType::GeometricType(GeometricTypeKind::GeometricPath), + 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(TypedString { + data_type: DataType::GeometricType(GeometricTypeKind::GeometricBox), + 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(TypedString { + data_type: DataType::GeometricType(GeometricTypeKind::Circle), + 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(TypedString { + data_type: DataType::GeometricType(GeometricTypeKind::Polygon), + 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(TypedString { + data_type: DataType::GeometricType(GeometricTypeKind::LineSegment), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) + ); +} +#[test] +fn test_geometric_binary_operators() { + // Translation plus + let sql = "box '((0,0),(1,1))' + point '(2.0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::Plus, + .. + } + )); + // Translation minus + let sql = "box '((0,0),(1,1))' - point '(2.0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::Minus, + .. + } + )); + + // Scaling multiply + let sql = "box '((0,0),(1,1))' * point '(2.0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::Multiply, + .. + } + )); + + // Scaling divide + let sql = "box '((0,0),(1,1))' / point '(2.0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::Divide, + .. + } + )); + + // Intersection + let sql = "'((1,-1),(-1,1))' # '((1,1),(-1,-1))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PGBitwiseXor, + .. + } + )); + + //Point of closest proximity + let sql = "point '(0,0)' ## lseg '((2,0),(0,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::DoubleHash, + .. + } + )); + + // Point of closest proximity + let sql = "box '((0,0),(1,1))' && box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PGOverlap, + .. + } + )); + + // Overlaps to left? + let sql = "box '((0,0),(1,1))' &< box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::AndLt, + .. + } + )); + + // Overlaps to right? + let sql = "box '((0,0),(3,3))' &> box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::AndGt, + .. + } + )); + + // Distance between + let sql = "circle '((0,0),1)' <-> circle '((5,0),1)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::LtDashGt, + .. + } + )); + + // Is left of? + let sql = "circle '((0,0),1)' << circle '((5,0),1)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PGBitwiseShiftLeft, + .. + } + )); + + // Is right of? + let sql = "circle '((5,0),1)' >> circle '((0,0),1)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PGBitwiseShiftRight, + .. + } + )); + + // Is below? + let sql = "circle '((0,0),1)' <^ circle '((0,5),1)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::LtCaret, + .. + } + )); + + // Intersects or overlaps + let sql = "lseg '((-1,0),(1,0))' ?# box '((-2,-2),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionHash, + .. + } + )); + + // Is horizontal? + let sql = "point '(1,0)' ?- point '(0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionDash, + .. + } + )); + + // Is perpendicular? + let sql = "lseg '((0,0),(0,1))' ?-| lseg '((0,0),(1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionDashPipe, + .. + } + )); + + // Is vertical? + let sql = "point '(0,1)' ?| point '(0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionPipe, + .. + } + )); + + // Are parallel? + let sql = "lseg '((-1,0),(1,0))' ?|| lseg '((-1,2),(1,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionDoublePipe, + .. + } + )); + + // Contained or on? + let sql = "point '(1,1)' @ circle '((0,0),2)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::At, + .. + } + )); + + // + // Same as? + let sql = "polygon '((0,0),(1,1))' ~= polygon '((1,1),(0,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::TildeEq, + .. + } + )); + + // Is strictly below? + let sql = "box '((0,0),(3,3))' <<| box '((3,4),(5,5))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::LtLtPipe, + .. + } + )); + + // Is strictly above? + let sql = "box '((3,4),(5,5))' |>> box '((0,0),(3,3))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PipeGtGt, + .. + } + )); + + // Does not extend above? + let sql = "box '((0,0),(1,1))' &<| box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::AndLtPipe, + .. + } + )); + + // Does not extend below? + let sql = "box '((0,0),(3,3))' |&> box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PipeAndGt, + .. + } + )); +} + +#[test] +fn parse_array_type_def_with_brackets() { + let dialects = all_dialects_where(|d| d.supports_array_typedef_with_brackets()); + dialects.verified_stmt("SELECT x::INT[]"); + dialects.verified_stmt("SELECT STRING_TO_ARRAY('1,2,3', ',')::INT[3]"); +} + +#[test] +fn parse_set_names() { + let dialects = all_dialects_where(|d| d.supports_set_names()); + dialects.verified_stmt("SET NAMES 'UTF8'"); + dialects.verified_stmt("SET NAMES 'utf8'"); + dialects.verified_stmt("SET NAMES UTF8 COLLATE bogus"); +} + +#[test] +fn parse_pipeline_operator() { + let dialects = all_dialects_where(|d| d.supports_pipe_operator()); + + // 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"); + + // 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", + ); + + // 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'"); + + // 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() { + match all_dialects().verified_stmt("SET TIME ZONE 'UTC'") { + Statement::Set(Set::SetTimeZone { local, value }) => { + assert!(!local); + assert_eq!( + value, + Expr::Value((Value::SingleQuotedString("UTC".into())).with_empty_span()) + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_return() { + let stmt = all_dialects().verified_stmt("RETURN"); + assert_eq!(stmt, Statement::Return(ReturnStatement { value: None })); + + 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(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), + default: None, + }, + ProcedureParam { + name: Ident { + value: "b".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Text, + mode: Some(ArgMode::Out), + default: None, + }, + ProcedureParam { + name: Ident { + value: "c".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Timestamp(None, TimezoneInfo::None), + mode: Some(ArgMode::InOut), + default: None, + }, + ProcedureParam { + name: Ident { + value: "d".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Bool, + mode: None, + default: None, + }, + ]) + ); + } + _ => unreachable!(), + } + + // parameters with default values + let sql = r#"CREATE PROCEDURE test_proc (IN a INTEGER = 1, OUT b TEXT = '2', INOUT c TIMESTAMP = NULL, d BOOL = 0) 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"); + assert_eq!( + params, + Some(vec![ + ProcedureParam { + name: Ident::new("a"), + data_type: DataType::Integer(None), + mode: Some(ArgMode::In), + default: Some(Expr::Value((number("1")).with_empty_span())), + }, + ProcedureParam { + name: Ident::new("b"), + data_type: DataType::Text, + mode: Some(ArgMode::Out), + default: Some(Expr::Value( + Value::SingleQuotedString("2".into()).with_empty_span() + )), + }, + ProcedureParam { + name: Ident::new("c"), + data_type: DataType::Timestamp(None, TimezoneInfo::None), + mode: Some(ArgMode::InOut), + default: Some(Expr::Value(Value::Null.with_empty_span())), + }, + ProcedureParam { + name: Ident::new("d"), + data_type: DataType::Bool, + mode: None, + default: Some(Expr::Value((number("0")).with_empty_span())), + } + ]), + ); + } + _ => 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'"); + let dialects = all_dialects_where(|d| d.supports_boolean_literals()); + dialects.one_statement_parses_to( + "CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=TRUE", + "CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=true", + ); + dialects.verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=true TYPE=SERVICE TAG (t1='v1')"); + let create = dialects.verified_stmt("CREATE OR REPLACE USER IF NOT EXISTS u1 PASSWORD='secret' MUST_CHANGE_PASSWORD=false 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(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "secret".to_string() + )), + }, + KeyValueOption { + option_name: "MUST_CHANGE_PASSWORD".to_string(), + option_value: KeyValueOptionKind::Single(Value::Boolean(false)), + }, + KeyValueOption { + option_name: "TYPE".to_string(), + option_value: KeyValueOptionKind::Single(Value::Placeholder( + "SERVICE".to_string() + )), + }, + ], + }, + ); + assert_eq!(stmt.with_tags, true); + assert_eq!( + stmt.tags, + KeyValueOptions { + delimiter: KeyValueOptionsDelimiter::Comma, + options: vec![ + KeyValueOption { + option_name: "t1".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "v1".to_string() + )), + }, + KeyValueOption { + option_name: "t2".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "v2".to_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 ", + "REMOVEQUOTES ", + "COMPUPDATE ", + "COMPUPDATE PRESET ", + "COMPUPDATE ON ", + "COMPUPDATE OFF ", + "COMPUPDATE TRUE ", + "COMPUPDATE FALSE ", + "STATUPDATE ", + "STATUPDATE ON ", + "STATUPDATE OFF ", + "STATUPDATE TRUE ", + "STATUPDATE FALSE", + ), + 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 ", + "REMOVEQUOTES ", + "COMPUPDATE ", + "COMPUPDATE PRESET ", + "COMPUPDATE TRUE ", + "COMPUPDATE FALSE ", + "COMPUPDATE TRUE ", + "COMPUPDATE FALSE ", + "STATUPDATE ", + "STATUPDATE TRUE ", + "STATUPDATE FALSE ", + "STATUPDATE TRUE ", + "STATUPDATE FALSE", + ), + ); + 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, + ), + ("SELECT * FROM SEMANTIC_VIEW(model METRICS orders.*)", None), + ("SELECT * FROM SEMANTIC_VIEW(model FACTS fact.*)", None), + ( + "SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim.* METRICS orders.*)", + 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)", + ]; + + 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"), + } +} + +#[test] +fn parse_adjacent_string_literal_concatenation() { + let sql = r#"SELECT 'M' "y" 'S' "q" 'l'"#; + let dialects = all_dialects_where(|d| d.supports_string_literal_concatenation()); + dialects.one_statement_parses_to(sql, r"SELECT 'MySql'"); + + let sql = "SELECT * FROM t WHERE col = 'Hello' \n ' ' \t 'World!'"; + dialects.one_statement_parses_to(sql, r"SELECT * FROM t WHERE col = 'Hello World!'"); +} + +#[test] +fn parse_invisible_column() { + let sql = r#"CREATE TABLE t (foo INT, bar INT INVISIBLE)"#; + let stmt = verified_stmt(sql); + match stmt { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ + ColumnDef { + name: "foo".into(), + data_type: DataType::Int(None), + options: vec![] + }, + ColumnDef { + name: "bar".into(), + data_type: DataType::Int(None), + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Invisible + }] + } + ] + ); + } + _ => panic!("Unexpected statement {stmt}"), + } + + let sql = r#"ALTER TABLE t ADD COLUMN bar INT INVISIBLE"#; + let stmt = verified_stmt(sql); + match stmt { + Statement::AlterTable(alter_table) => { + assert_eq!( + alter_table.operations, + vec![AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "bar".into(), + data_type: DataType::Int(None), + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Invisible + }] + }, + column_position: None + }] + ); + } + _ => panic!("Unexpected statement {stmt}"), + } +} + +#[test] +fn parse_create_index_different_using_positions() { + let sql = "CREATE INDEX idx_name USING BTREE ON table_name (col1)"; + let expected = "CREATE INDEX idx_name ON table_name USING BTREE (col1)"; + match all_dialects().one_statement_parses_to(sql, expected) { + Statement::CreateIndex(CreateIndex { + name, + table_name, + using, + columns, + unique, + .. + }) => { + assert_eq!(name.unwrap().to_string(), "idx_name"); + assert_eq!(table_name.to_string(), "table_name"); + assert_eq!(using, Some(IndexType::BTree)); + assert_eq!(columns.len(), 1); + assert!(!unique); + } + _ => unreachable!(), + } + + let sql = "CREATE INDEX idx_name USING BTREE ON table_name (col1) USING HASH"; + let expected = "CREATE INDEX idx_name ON table_name USING BTREE (col1) USING HASH"; + match all_dialects().one_statement_parses_to(sql, expected) { + Statement::CreateIndex(CreateIndex { + name, + table_name, + columns, + index_options, + .. + }) => { + assert_eq!(name.unwrap().to_string(), "idx_name"); + assert_eq!(table_name.to_string(), "table_name"); + assert_eq!(columns.len(), 1); + assert!(index_options + .iter() + .any(|o| o == &IndexOption::Using(IndexType::Hash))); + } + _ => unreachable!(), + } +} + +#[test] +fn test_parse_alter_user() { + verified_stmt("ALTER USER u1"); + verified_stmt("ALTER USER IF EXISTS u1"); + let stmt = verified_stmt("ALTER USER IF EXISTS u1 RENAME TO u2"); + match stmt { + Statement::AlterUser(alter) => { + assert!(alter.if_exists); + assert_eq!(alter.name, Ident::new("u1")); + assert_eq!(alter.rename_to, Some(Ident::new("u2"))); + } + _ => unreachable!(), + } + verified_stmt("ALTER USER IF EXISTS u1 RESET PASSWORD"); + verified_stmt("ALTER USER IF EXISTS u1 ABORT ALL QUERIES"); + verified_stmt( + "ALTER USER IF EXISTS u1 ADD DELEGATED AUTHORIZATION OF ROLE r1 TO SECURITY INTEGRATION i1", + ); + verified_stmt("ALTER USER IF EXISTS u1 REMOVE DELEGATED AUTHORIZATION OF ROLE r1 FROM SECURITY INTEGRATION i1"); + verified_stmt( + "ALTER USER IF EXISTS u1 REMOVE DELEGATED AUTHORIZATIONS FROM SECURITY INTEGRATION i1", + ); + verified_stmt("ALTER USER IF EXISTS u1 ENROLL MFA"); + let stmt = verified_stmt("ALTER USER u1 SET DEFAULT_MFA_METHOD PASSKEY"); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!(alter.set_default_mfa_method, Some(MfaMethodKind::PassKey)) + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 SET DEFAULT_MFA_METHOD TOTP"); + verified_stmt("ALTER USER u1 SET DEFAULT_MFA_METHOD DUO"); + let stmt = verified_stmt("ALTER USER u1 REMOVE MFA METHOD PASSKEY"); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!(alter.remove_mfa_method, Some(MfaMethodKind::PassKey)) + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 REMOVE MFA METHOD TOTP"); + verified_stmt("ALTER USER u1 REMOVE MFA METHOD DUO"); + let stmt = verified_stmt("ALTER USER u1 MODIFY MFA METHOD PASSKEY SET COMMENT 'abc'"); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!( + alter.modify_mfa_method, + Some(AlterUserModifyMfaMethod { + method: MfaMethodKind::PassKey, + comment: "abc".to_string() + }) + ); + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 ADD MFA METHOD OTP"); + verified_stmt("ALTER USER u1 ADD MFA METHOD OTP COUNT = 8"); + + let stmt = verified_stmt("ALTER USER u1 SET AUTHENTICATION POLICY p1"); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!( + alter.set_policy, + Some(AlterUserSetPolicy { + policy_kind: UserPolicyKind::Authentication, + policy: Ident::new("p1") + }) + ); + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 SET PASSWORD POLICY p1"); + verified_stmt("ALTER USER u1 SET SESSION POLICY p1"); + let stmt = verified_stmt("ALTER USER u1 UNSET AUTHENTICATION POLICY"); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!(alter.unset_policy, Some(UserPolicyKind::Authentication)); + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 UNSET PASSWORD POLICY"); + verified_stmt("ALTER USER u1 UNSET SESSION POLICY"); + + let stmt = verified_stmt("ALTER USER u1 SET TAG k1='v1'"); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!( + alter.set_tag.options, + vec![KeyValueOption { + option_name: "k1".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "v1".to_string() + )), + },] + ); + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 SET TAG k1='v1', k2='v2'"); + let stmt = verified_stmt("ALTER USER u1 UNSET TAG k1"); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!(alter.unset_tag, vec!["k1".to_string()]); + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 UNSET TAG k1, k2, k3"); + + let dialects = all_dialects_where(|d| d.supports_boolean_literals()); + dialects.one_statement_parses_to( + "ALTER USER u1 SET PASSWORD='secret', MUST_CHANGE_PASSWORD=TRUE, MINS_TO_UNLOCK=10", + "ALTER USER u1 SET PASSWORD='secret', MUST_CHANGE_PASSWORD=true, MINS_TO_UNLOCK=10", + ); + + let stmt = dialects.verified_stmt( + "ALTER USER u1 SET PASSWORD='secret', MUST_CHANGE_PASSWORD=true, MINS_TO_UNLOCK=10", + ); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!( + alter.set_props, + KeyValueOptions { + delimiter: KeyValueOptionsDelimiter::Comma, + options: vec![ + KeyValueOption { + option_name: "PASSWORD".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "secret".to_string() + )), + }, + KeyValueOption { + option_name: "MUST_CHANGE_PASSWORD".to_string(), + option_value: KeyValueOptionKind::Single(Value::Boolean(true)), + }, + KeyValueOption { + option_name: "MINS_TO_UNLOCK".to_string(), + option_value: KeyValueOptionKind::Single(number("10")), + }, + ] + } + ); + } + _ => unreachable!(), + } + + let stmt = verified_stmt("ALTER USER u1 UNSET PASSWORD"); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!(alter.unset_props, vec!["PASSWORD".to_string()]); + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 UNSET PASSWORD, MUST_CHANGE_PASSWORD, MINS_TO_UNLOCK"); + + let stmt = verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=('ALL')"); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!( + alter.set_props.options, + vec![KeyValueOption { + option_name: "DEFAULT_SECONDARY_ROLES".to_string(), + option_value: KeyValueOptionKind::Multi(vec![Value::SingleQuotedString( + "ALL".to_string() + )]) + }] + ); + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=()"); + verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=('R1', 'R2', 'R3')"); + verified_stmt("ALTER USER u1 SET PASSWORD='secret', DEFAULT_SECONDARY_ROLES=('ALL')"); + verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=('ALL'), PASSWORD='secret'"); + let stmt = verified_stmt( + "ALTER USER u1 SET WORKLOAD_IDENTITY=(TYPE=AWS, ARN='arn:aws:iam::123456789:r1/')", + ); + match stmt { + Statement::AlterUser(alter) => { + assert_eq!( + alter.set_props.options, + vec![KeyValueOption { + option_name: "WORKLOAD_IDENTITY".to_string(), + option_value: KeyValueOptionKind::KeyValueOptions(Box::new(KeyValueOptions { + delimiter: KeyValueOptionsDelimiter::Comma, + options: vec![ + KeyValueOption { + option_name: "TYPE".to_string(), + option_value: KeyValueOptionKind::Single(Value::Placeholder( + "AWS".to_string() + )), + }, + KeyValueOption { + option_name: "ARN".to_string(), + option_value: KeyValueOptionKind::Single( + Value::SingleQuotedString( + "arn:aws:iam::123456789:r1/".to_string() + ) + ), + }, + ] + })) + }] + ) + } + _ => unreachable!(), + } + verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=('ALL'), PASSWORD='secret', WORKLOAD_IDENTITY=(TYPE=AWS, ARN='arn:aws:iam::123456789:r1/')"); +} + +#[test] +fn parse_generic_unary_ops() { + let unary_ops = &[ + ("~", UnaryOperator::BitwiseNot), + ("-", UnaryOperator::Minus), + ("+", UnaryOperator::Plus), + ]; + for (str_op, op) in unary_ops { + let select = verified_only_select(&format!("SELECT {}expr", &str_op)); + assert_eq!( + UnnamedExpr(UnaryOp { + op: *op, + expr: Box::new(Identifier(Ident::new("expr"))), + }), + select.projection[0] + ); + } +} + +#[test] +fn parse_reset_statement() { + match verified_stmt("RESET some_parameter") { + Statement::Reset(ResetStatement { + reset: Reset::ConfigurationParameter(o), + }) => assert_eq!(o, ObjectName::from(vec!["some_parameter".into()])), + _ => unreachable!(), + } + match verified_stmt("RESET some_extension.some_parameter") { + Statement::Reset(ResetStatement { + reset: Reset::ConfigurationParameter(o), + }) => assert_eq!( + o, + ObjectName::from(vec!["some_extension".into(), "some_parameter".into()]) + ), + _ => unreachable!(), + } + match verified_stmt("RESET ALL") { + Statement::Reset(ResetStatement { reset }) => assert_eq!(reset, Reset::ALL), + _ => unreachable!(), + } +} + +#[test] +fn test_parse_set_session_authorization() { + let stmt = verified_stmt("SET SESSION AUTHORIZATION DEFAULT"); + assert_eq!( + stmt, + Statement::Set(Set::SetSessionAuthorization(SetSessionAuthorizationParam { + scope: ContextModifier::Session, + kind: SetSessionAuthorizationParamKind::Default, + })) + ); + + let stmt = verified_stmt("SET SESSION AUTHORIZATION 'username'"); + assert_eq!( + stmt, + Statement::Set(Set::SetSessionAuthorization(SetSessionAuthorizationParam { + scope: ContextModifier::Session, + kind: SetSessionAuthorizationParamKind::User(Ident { + value: "username".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + })) + ); +} diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index 61874fc27..cee604aca 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -41,7 +41,7 @@ fn custom_prefix_parser() -> Result<(), ParserError> { fn parse_prefix(&self, parser: &mut Parser) -> Option> { if parser.consume_token(&Token::Number("1".to_string(), false)) { - Some(Ok(Expr::Value(Value::Null))) + Some(Ok(Expr::Value(Value::Null.with_empty_span()))) } else { None } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index b9ca55d13..065e8f9e7 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] @@ -47,7 +49,9 @@ fn test_databricks_identifiers() { databricks() .verified_only_select(r#"SELECT "Ä""#) .projection[0], - SelectItem::UnnamedExpr(Expr::Value(Value::DoubleQuotedString("Ä".to_owned()))) + SelectItem::UnnamedExpr(Expr::Value( + (Value::DoubleQuotedString("Ä".to_owned())).with_empty_span() + )) ); } @@ -62,9 +66,9 @@ fn test_databricks_exists() { call( "array", [ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")) + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")) ] ), Expr::Lambda(LambdaFunction { @@ -99,40 +103,44 @@ fn test_databricks_lambdas() { call( "array", [ - Expr::Value(Value::SingleQuotedString("Hello".to_owned())), - Expr::Value(Value::SingleQuotedString("World".to_owned())) + Expr::value(Value::SingleQuotedString("Hello".to_owned())), + Expr::value(Value::SingleQuotedString("World".to_owned())) ] ), 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![ - Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("p1"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::Identifier(Ident::new("p2"))) + CaseWhen { + condition: Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("p1"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("p2"))) + }, + result: Expr::value(number("0")) + }, + CaseWhen { + condition: Expr::BinaryOp { + left: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p1"))] + )), + op: BinaryOperator::Lt, + right: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p2"))] + )), + }, + result: Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::value(number("1"))) + } }, - Expr::BinaryOp { - left: Box::new(call( - "reverse", - [Expr::Identifier(Ident::new("p1"))] - )), - op: BinaryOperator::Lt, - right: Box::new(call( - "reverse", - [Expr::Identifier(Ident::new("p2"))] - )) - } - ], - results: vec![ - Expr::Value(number("0")), - Expr::UnaryOp { - op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(number("1"))) - } ], - else_result: Some(Box::new(Expr::Value(number("1")))) + else_result: Some(Box::new(Expr::value(number("1")))) }) }) ] @@ -149,15 +157,16 @@ fn test_databricks_lambdas() { #[test] fn test_values_clause() { let values = Values { + value_keyword: false, explicit_row: false, rows: vec![ vec![ - Expr::Value(Value::DoubleQuotedString("one".to_owned())), - Expr::Value(number("1")), + Expr::Value((Value::DoubleQuotedString("one".to_owned())).with_empty_span()), + Expr::value(number("1")), ], vec![ - Expr::Value(Value::SingleQuotedString("two".to_owned())), - Expr::Value(number("2")), + Expr::Value((Value::SingleQuotedString("two".to_owned())).with_empty_span()), + Expr::value(number("2")), ], ], }; @@ -185,7 +194,9 @@ fn test_values_clause() { "SELECT * FROM values", )); assert_eq!( - Some(&table_from_name(ObjectName(vec![Ident::new("values")]))), + Some(&table_from_name(ObjectName::from(vec![Ident::new( + "values" + )]))), query .body .as_select() @@ -204,16 +215,16 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - databricks().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + databricks().verified_stmt(&format!("USE {object_name}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); 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)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + databricks().verified_stmt(&format!("USE {quote}{object_name}{quote}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), )]))) @@ -224,22 +235,22 @@ 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)), - Statement::Use(Use::Catalog(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Database(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Schema(ObjectName(vec![Ident::with_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(), )]))) @@ -249,15 +260,19 @@ fn parse_use() { // Test single identifier with keyword and no quotes assert_eq!( databricks().verified_stmt("USE CATALOG my_catalog"), - Statement::Use(Use::Catalog(ObjectName(vec![Ident::new("my_catalog")]))) + Statement::Use(Use::Catalog(ObjectName::from(vec![Ident::new( + "my_catalog" + )]))) ); assert_eq!( databricks().verified_stmt("USE DATABASE my_schema"), - Statement::Use(Use::Database(ObjectName(vec![Ident::new("my_schema")]))) + Statement::Use(Use::Database(ObjectName::from(vec![Ident::new( + "my_schema" + )]))) ); assert_eq!( databricks().verified_stmt("USE SCHEMA my_schema"), - Statement::Use(Use::Schema(ObjectName(vec![Ident::new("my_schema")]))) + Statement::Use(Use::Schema(ObjectName::from(vec![Ident::new("my_schema")]))) ); // Test invalid syntax - missing identifier @@ -278,8 +293,8 @@ fn parse_databricks_struct_function() { .projection[0], SelectItem::UnnamedExpr(Expr::Struct { values: vec![ - Expr::Value(number("1")), - Expr::Value(Value::SingleQuotedString("foo".to_string())) + Expr::value(number("1")), + Expr::Value((Value::SingleQuotedString("foo".to_string())).with_empty_span()) ], fields: vec![] }) @@ -291,16 +306,63 @@ fn parse_databricks_struct_function() { SelectItem::UnnamedExpr(Expr::Struct { values: vec![ Expr::Named { - expr: Expr::Value(number("1")).into(), + expr: Expr::value(number("1")).into(), name: Ident::new("one") }, Expr::Named { - expr: Expr::Value(Value::SingleQuotedString("foo".to_string())).into(), + expr: Expr::Value( + (Value::SingleQuotedString("foo".to_string())).with_empty_span() + ) + .into(), name: Ident::new("foo") }, - Expr::Value(Value::Boolean(false)) + Expr::Value((Value::Boolean(false)).with_empty_span()) ], fields: vec![] }) ); } + +#[test] +fn data_type_timestamp_ntz() { + // Literal + assert_eq!( + databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"), + Expr::TypedString(TypedString { + data_type: DataType::TimestampNtz(None), + value: ValueWithSpan { + value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()), + span: Span::empty(), + }, + uses_odbc_syntax: false + }) + ); + + // Cast + assert_eq!( + databricks().verified_expr("(created_at)::TIMESTAMP_NTZ"), + Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Nested(Box::new(Expr::Identifier( + "created_at".into() + )))), + data_type: DataType::TimestampNtz(None), + format: None + } + ); + + // Column definition + match databricks().verified_stmt("CREATE TABLE foo (x TIMESTAMP_NTZ)") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "x".into(), + data_type: DataType::TimestampNtz(None), + options: vec![], + }] + ); + } + s => panic!("Unexpected statement: {s:?}"), + } +} diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index db4ffb6f6..0f8051955 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, @@ -60,7 +63,6 @@ fn test_struct() { vec![ColumnDef { name: "s".into(), data_type: struct_type1.clone(), - collation: None, options: vec![], }] ); @@ -75,7 +77,6 @@ fn test_struct() { Box::new(struct_type1), None )), - collation: None, options: vec![], }] ); @@ -86,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")), @@ -94,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, @@ -120,7 +125,6 @@ fn test_struct() { Box::new(struct_type2), None )), - collation: None, options: vec![], }] ); @@ -160,7 +164,7 @@ fn test_select_wildcard_with_exclude() { let select = duckdb().verified_only_select("SELECT name.* EXCLUDE department_id FROM employee_table"); let expected = SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("name")]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])), WildcardAdditionalOptions { opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), ..Default::default() @@ -191,7 +195,7 @@ fn test_create_macro() { let expected = Statement::CreateMacro { or_replace: false, temporary: false, - name: ObjectName(vec![Ident::new("schema"), Ident::new("add")]), + name: ObjectName::from(vec![Ident::new("schema"), Ident::new("add")]), args: Some(vec![MacroArg::new("a"), MacroArg::new("b")]), definition: MacroDefinition::Expr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("a"))), @@ -208,12 +212,12 @@ fn test_create_macro_default_args() { let expected = Statement::CreateMacro { or_replace: false, temporary: false, - name: ObjectName(vec![Ident::new("add_default")]), + name: ObjectName::from(vec![Ident::new("add_default")]), args: Some(vec![ MacroArg::new("a"), MacroArg { name: Ident::new("b"), - default_expr: Some(Expr::Value(number("5"))), + default_expr: Some(Expr::value(number("5"))), }, ]), definition: MacroDefinition::Expr(Expr::BinaryOp { @@ -236,7 +240,7 @@ fn test_create_table_macro() { let expected = Statement::CreateMacro { or_replace: true, temporary: true, - name: ObjectName(vec![Ident::new("dynamic_table")]), + name: ObjectName::from(vec![Ident::new("dynamic_table")]), args: Some(vec![ MacroArg::new("col1_value"), MacroArg::new("col2_value"), @@ -265,10 +269,11 @@ 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 { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "capitals".to_string(), quote_style: None, span: Span::empty(), @@ -288,16 +293,18 @@ fn test_select_union_by_name() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), right: Box::::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "weather".to_string(), quote_style: None, span: Span::empty(), @@ -317,6 +324,7 @@ fn test_select_union_by_name() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), }); assert_eq!(ast.body, expected); @@ -353,6 +361,32 @@ fn test_duckdb_load_extension() { ); } +#[test] +fn test_duckdb_specific_int_types() { + let duckdb_dtypes = vec![ + ("UTINYINT", DataType::UTinyInt), + ("USMALLINT", DataType::USmallInt), + ("UBIGINT", DataType::UBigInt), + ("UHUGEINT", DataType::UHugeInt), + ("HUGEINT", DataType::HugeInt), + ]; + for (dtype_string, data_type) in duckdb_dtypes { + let sql = format!("SELECT 123::{dtype_string}"); + let select = duckdb().verified_only_select(&sql); + assert_eq!( + &Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Value( + Value::Number("123".parse().unwrap(), false).with_empty_span() + )), + data_type: data_type.clone(), + format: None, + }, + expr_from_projection(&select.projection[0]) + ); + } +} + #[test] fn test_duckdb_struct_literal() { //struct literal syntax https://duckdb.org/docs/sql/data_types/struct#creating-structs @@ -364,15 +398,15 @@ fn test_duckdb_struct_literal() { &Expr::Dictionary(vec![ DictionaryField { key: Ident::with_quote('\'', "a"), - value: Box::new(Expr::Value(number("1"))), + value: Box::new(Expr::value(number("1"))), }, DictionaryField { key: Ident::with_quote('\'', "b"), - value: Box::new(Expr::Value(number("2"))), + value: Box::new(Expr::value(number("2"))), }, DictionaryField { key: Ident::with_quote('\'', "c"), - value: Box::new(Expr::Value(number("3"))), + value: Box::new(Expr::value(number("3"))), }, ],), expr_from_projection(&select.projection[0]) @@ -382,7 +416,9 @@ fn test_duckdb_struct_literal() { &Expr::Array(Array { elem: vec![Expr::Dictionary(vec![DictionaryField { key: Ident::with_quote('\'', "a"), - value: Box::new(Expr::Value(Value::SingleQuotedString("abc".to_string()))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("abc".to_string())).with_empty_span() + )), },],)], named: false }), @@ -392,7 +428,7 @@ fn test_duckdb_struct_literal() { &Expr::Dictionary(vec![ DictionaryField { key: Ident::with_quote('\'', "a"), - value: Box::new(Expr::Value(number("1"))), + value: Box::new(Expr::value(number("1"))), }, DictionaryField { key: Ident::with_quote('\'', "b"), @@ -411,11 +447,14 @@ fn test_duckdb_struct_literal() { &Expr::Dictionary(vec![ DictionaryField { key: Ident::with_quote('\'', "a"), - value: Expr::Value(number("1")).into(), + value: Expr::value(number("1")).into(), }, DictionaryField { key: Ident::with_quote('\'', "b"), - value: Expr::Value(Value::SingleQuotedString("abc".to_string())).into(), + value: Expr::Value( + (Value::SingleQuotedString("abc".to_string())).with_empty_span() + ) + .into(), }, ],), expr_from_projection(&select.projection[3]) @@ -432,7 +471,7 @@ fn test_duckdb_struct_literal() { key: Ident::with_quote('\'', "a"), value: Expr::Dictionary(vec![DictionaryField { key: Ident::with_quote('\'', "aa"), - value: Expr::Value(number("1")).into(), + value: Expr::value(number("1")).into(), }],) .into(), }],), @@ -587,7 +626,7 @@ fn test_duckdb_named_argument_function_with_assignment_operator() { let select = duckdb_and_generic().verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("FUN")]), + name: ObjectName::from(vec![Ident::new("FUN")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -595,16 +634,16 @@ fn test_duckdb_named_argument_function_with_assignment_operator() { args: vec![ FunctionArg::Named { name: Ident::new("a"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "1".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("1".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::Assignment }, FunctionArg::Named { name: Ident::new("b"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "2".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("2".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::Assignment }, ], @@ -633,14 +672,14 @@ fn test_array_index() { &Expr::CompoundFieldAccess { root: Box::new(Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("a".to_owned())), - Expr::Value(Value::SingleQuotedString("b".to_owned())), - Expr::Value(Value::SingleQuotedString("c".to_owned())) + Expr::Value((Value::SingleQuotedString("a".to_owned())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("b".to_owned())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("c".to_owned())).with_empty_span()) ], named: false })), access_chain: vec![AccessExpr::Subscript(Subscript::Index { - index: Expr::Value(number("3")) + index: Expr::value(number("3")) })] }, expr @@ -660,7 +699,9 @@ fn test_duckdb_union_datatype() { if_not_exists: Default::default(), transient: Default::default(), volatile: Default::default(), - name: ObjectName(vec!["tbl1".into()]), + iceberg: Default::default(), + dynamic: Default::default(), + name: ObjectName::from(vec!["tbl1".into()]), columns: vec![ ColumnDef { name: "one".into(), @@ -668,7 +709,6 @@ fn test_duckdb_union_datatype() { field_name: "a".into(), field_type: DataType::Int(None) }]), - collation: Default::default(), options: Default::default() }, ColumnDef { @@ -683,7 +723,6 @@ fn test_duckdb_union_datatype() { field_type: DataType::Int(None) } ]), - collation: Default::default(), options: Default::default() }, ColumnDef { @@ -695,7 +734,6 @@ fn test_duckdb_union_datatype() { field_type: DataType::Int(None) }]) }]), - collation: Default::default(), options: Default::default() } ], @@ -707,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(), @@ -727,7 +759,7 @@ 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(), enable_schema_evolution: Default::default(), @@ -737,7 +769,19 @@ fn test_duckdb_union_datatype() { default_ddl_collation: Default::default(), with_aggregation_policy: Default::default(), with_row_access_policy: Default::default(), - with_tags: Default::default() + with_tags: Default::default(), + base_location: Default::default(), + external_volume: Default::default(), + 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 ); @@ -758,16 +802,16 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + duckdb().verified_stmt(&format!("USE {object_name}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); 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)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + duckdb().verified_stmt(&format!("USE {quote}{object_name}{quote}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), )]))) @@ -778,8 +822,10 @@ 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)), - Statement::Use(Use::Object(ObjectName(vec![ + 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") ]))) @@ -788,9 +834,44 @@ fn parse_use() { // Test double identifier without quotes assert_eq!( duckdb().verified_stmt("USE mydb.my_schema"), - Statement::Use(Use::Object(ObjectName(vec![ + Statement::Use(Use::Object(ObjectName::from(vec![ Ident::new("mydb"), Ident::new("my_schema") ]))) ); } + +#[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() + ); +} + +#[test] +fn parse_extract_single_quotes() { + let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table"; + duckdb().verified_stmt(sql); +} diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 5349f1207..56a72ec84 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -22,11 +22,10 @@ use sqlparser::ast::{ ClusteredBy, CommentDef, CreateFunction, CreateFunctionBody, CreateFunctionUsing, CreateTable, - Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, - OneOrManyWithParens, OrderByExpr, SelectItem, Statement, TableFactor, UnaryOperator, Use, - Value, + Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, OrderByExpr, + OrderByOptions, SelectItem, Set, Statement, TableFactor, UnaryOperator, Use, Value, }; -use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; +use sqlparser::dialect::{AnsiDialect, GenericDialect, HiveDialect}; use sqlparser::parser::ParserError; use sqlparser::test_utils::*; @@ -92,7 +91,7 @@ fn parse_msck() { } #[test] -fn parse_set() { +fn parse_set_hivevar() { let set = "SET HIVEVAR:name = a, b, c_d"; hive().verified_stmt(set); } @@ -134,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!(), @@ -171,14 +168,18 @@ fn create_table_with_clustered_by() { sorted_by: Some(vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("a")), - asc: Some(true), - nulls_first: None, + options: OrderByOptions { + asc: Some(true), + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("b")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }, ]), @@ -340,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] @@ -365,20 +369,20 @@ fn from_cte() { fn set_statement_with_minus() { assert_eq!( hive().verified_stmt("SET hive.tez.java.opts = -Xmx4g"), - Statement::SetVariable { - local: false, + Statement::Set(Set::SingleAssignment { + scope: None, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![ + variable: ObjectName::from(vec![ Ident::new("hive"), Ident::new("tez"), Ident::new("java"), Ident::new("opts") - ])), - value: vec![Expr::UnaryOp { + ]), + values: vec![Expr::UnaryOp { op: UnaryOperator::Minus, expr: Box::new(Expr::Identifier(Ident::new("Xmx4g"))) }], - } + }) ); assert_eq!( @@ -405,7 +409,8 @@ fn parse_create_function() { assert_eq!( function_body, Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - Value::SingleQuotedString("org.random.class.Name".to_string()) + (Value::SingleQuotedString("org.random.class.Name".to_string())) + .with_empty_span() ))) ); assert_eq!( @@ -419,7 +424,7 @@ fn parse_create_function() { } // Test error in dialect that doesn't support parsing CREATE FUNCTION - let unsupported_dialects = TestedDialects::new(vec![Box::new(MsSqlDialect {})]); + let unsupported_dialects = TestedDialects::new(vec![Box::new(AnsiDialect {})]); assert_eq!( unsupported_dialects.parse_sql_statements(sql).unwrap_err(), @@ -460,8 +465,12 @@ fn parse_delimited_identifiers() { partitions: _, json_path: _, sample: _, + index_hints: _, } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -480,7 +489,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -515,16 +524,16 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - hive().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + hive().verified_stmt(&format!("USE {object_name}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - hive().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_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 567cd5382..a947db49b 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -23,15 +23,16 @@ mod test_utils; use helpers::attached_token::AttachedToken; -use sqlparser::tokenizer::Span; +use sqlparser::keywords::Keyword; +use sqlparser::tokenizer::{Location, Span, Token, TokenWithSpan, Word}; use test_utils::*; -use sqlparser::ast::DataType::{Int, Text}; +use sqlparser::ast::DataType::{Int, Text, Varbinary}; use sqlparser::ast::DeclareAssignment::MsSqlAssignment; use sqlparser::ast::Value::SingleQuotedString; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MsSqlDialect}; -use sqlparser::parser::ParserError; +use sqlparser::parser::{Parser, ParserError, ParserOptions}; #[test] fn parse_mssql_identifiers() { @@ -63,17 +64,18 @@ fn parse_table_time_travel() { select.from, vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t1")]), + name: ObjectName::from(vec![Ident::new("t1")]), alias: None, args: None, with_hints: vec![], version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( - Value::SingleQuotedString(version) + (Value::SingleQuotedString(version)).with_empty_span() ))), partitions: vec![], with_ordinality: false, json_path: None, sample: None, + index_hints: vec![] }, joins: vec![] },] @@ -98,46 +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: None, - limit_by: vec![], - offset: 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")))], - 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, - }))) - }))], + 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 { @@ -145,7 +154,9 @@ fn parse_create_procedure() { quote_style: None, span: Span::empty(), }, - data_type: DataType::Int(None) + data_type: DataType::Int(None), + mode: None, + default: None, }, ProcedureParam { name: Ident { @@ -156,33 +167,264 @@ fn parse_create_procedure() { data_type: DataType::Varchar(Some(CharacterLength::IntegerLength { length: 256, unit: None - })) + })), + mode: None, + default: None, } ]), - name: ObjectName(vec![Ident { + 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"); + + // parameters with default values + let sql = r#"CREATE PROCEDURE foo (IN @a INTEGER = 1, OUT @b TEXT = '2', INOUT @c DATETIME = NULL, @d BOOL = 0) AS BEGIN SELECT 1; END"#; + let _ = ms().verified_stmt(sql); +} + +#[test] +fn parse_create_function() { + let return_expression_function = "CREATE FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) RETURNS INT AS BEGIN RETURN 1; END"; + assert_eq!( + ms().verified_stmt(return_expression_function), + sqlparser::ast::Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: false, + temporary: false, + if_not_exists: false, + name: ObjectName::from(vec![Ident::new("some_scalar_udf")]), + args: Some(vec![ + OperateFunctionArg { + mode: None, + name: Some(Ident::new("@foo")), + data_type: DataType::Int(None), + default_expr: None, + }, + OperateFunctionArg { + mode: None, + name: Some(Ident::new("@bar")), + data_type: DataType::Varchar(Some(CharacterLength::IntegerLength { + length: 256, + unit: None + })), + default_expr: None, + }, + ]), + 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::Value( + (number("1")).with_empty_span() + ))), + })], + end_token: AttachedToken::empty(), + })), + behavior: None, + called_on_null: None, + parallel: None, + using: None, + language: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }), + ); + + let multi_statement_function = "\ + CREATE FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) \ + RETURNS INT \ + AS \ + BEGIN \ + SET @foo = @foo + 1; \ + RETURN @foo; \ + END\ + "; + 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 \ + AS \ + BEGIN \ + IF 1 = 2 \ + BEGIN \ + RETURN 1; \ + END; \ + RETURN 0; \ + END\ + "; + let _ = ms().verified_stmt(create_function_with_conditional); + + let create_or_alter_function = "\ + CREATE OR ALTER FUNCTION some_scalar_udf(@foo INT, @bar VARCHAR(256)) \ + RETURNS INT \ + AS \ + BEGIN \ + SET @foo = @foo + 1; \ + RETURN @foo; \ + 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] @@ -211,7 +453,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table")]), + name: ObjectName::from(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -223,6 +465,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![] }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -270,7 +513,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table"),]), + name: ObjectName::from(vec![Ident::new("t_test_table"),]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -282,6 +525,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![] }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -329,8 +573,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table")]), - + name: ObjectName::from(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -342,6 +585,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![] }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -389,7 +633,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table")]), + name: ObjectName::from(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -401,6 +645,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![], }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -428,7 +673,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table")]), + name: ObjectName::from(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -440,6 +685,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![], }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -467,7 +713,9 @@ fn parse_mssql_top_paren() { let select = ms_and_generic().verified_only_select(sql); let top = select.top.unwrap(); assert_eq!( - Some(TopQuantity::Expr(Expr::Value(number("5")))), + Some(TopQuantity::Expr(Expr::Value( + (number("5")).with_empty_span() + ))), top.quantity ); assert!(!top.percent); @@ -479,7 +727,9 @@ fn parse_mssql_top_percent() { let select = ms_and_generic().verified_only_select(sql); let top = select.top.unwrap(); assert_eq!( - Some(TopQuantity::Expr(Expr::Value(number("5")))), + Some(TopQuantity::Expr(Expr::Value( + (number("5")).with_empty_span() + ))), top.quantity ); assert!(top.percent); @@ -491,7 +741,9 @@ fn parse_mssql_top_with_ties() { let select = ms_and_generic().verified_only_select(sql); let top = select.top.unwrap(); assert_eq!( - Some(TopQuantity::Expr(Expr::Value(number("5")))), + Some(TopQuantity::Expr(Expr::Value( + (number("5")).with_empty_span() + ))), top.quantity ); assert!(top.with_ties); @@ -503,7 +755,9 @@ fn parse_mssql_top_percent_with_ties() { let select = ms_and_generic().verified_only_select(sql); let top = select.top.unwrap(); assert_eq!( - Some(TopQuantity::Expr(Expr::Value(number("10")))), + Some(TopQuantity::Expr(Expr::Value( + (number("10")).with_empty_span() + ))), top.quantity ); assert!(top.percent); @@ -524,15 +778,11 @@ fn parse_mssql_bin_literal() { fn parse_mssql_create_role() { let sql = "CREATE ROLE mssql AUTHORIZATION helena"; match ms().verified_stmt(sql) { - Statement::CreateRole { - names, - authorization_owner, - .. - } => { - assert_eq_vec(&["mssql"], &names); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["mssql"], &create_role.names); assert_eq!( - authorization_owner, - Some(ObjectName(vec![Ident { + create_role.authorization_owner, + Some(ObjectName::from(vec![Ident { value: "helena".into(), quote_style: None, span: Span::empty(), @@ -619,7 +869,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -638,7 +891,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -671,11 +924,11 @@ fn parse_table_name_in_square_brackets() { let select = ms().verified_only_select(r#"SELECT [a column] FROM [a schema].[a table]"#); if let TableFactor::Table { name, .. } = only(select.from).relation { assert_eq!( - vec![ + ObjectName::from(vec![ Ident::with_quote('[', "a schema"), Ident::with_quote('[', "a table") - ], - name.0 + ]), + name ); } else { panic!("Expecting TableFactor::Table"); @@ -737,7 +990,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[0], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::Function(_)), operator: FunctionArgOperator::Colon } @@ -753,7 +1009,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[2], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::Subquery(_)), operator: FunctionArgOperator::Colon } @@ -784,7 +1043,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[0], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::CompoundIdentifier(_)), operator: FunctionArgOperator::Colon } @@ -792,7 +1054,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[1], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::CompoundIdentifier(_)), operator: FunctionArgOperator::Colon } @@ -800,7 +1065,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[2], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::CompoundIdentifier(_)), operator: FunctionArgOperator::Colon } @@ -821,11 +1089,17 @@ fn parse_mssql_json_array() { assert_eq!( &[ FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".into()) + (Value::SingleQuotedString("a".into())).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("1")).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (Value::Null).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("2")).with_empty_span() ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Null))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), ], &args[..] ); @@ -847,11 +1121,17 @@ fn parse_mssql_json_array() { assert_eq!( &[ FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".into()) + (Value::SingleQuotedString("a".into())).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("1")).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (Value::Null).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("2")).with_empty_span() ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Null))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), ], &args[..] ); @@ -906,7 +1186,7 @@ fn parse_mssql_json_array() { }) => { assert_eq!( &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".into()) + (Value::SingleQuotedString("a".into())).with_empty_span() ))), &args[0] ); @@ -933,7 +1213,7 @@ fn parse_mssql_json_array() { }) => { assert_eq!( &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".into()) + (Value::SingleQuotedString("a".into())).with_empty_span() ))), &args[0] ); @@ -955,7 +1235,9 @@ fn parse_mssql_json_array() { .. }) => { assert_eq!( - &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("1")).with_empty_span() + ))), &args[0] ); assert!(matches!( @@ -1033,15 +1315,15 @@ fn parse_convert() { unreachable!() }; assert!(!is_try); - assert_eq!(Expr::Value(number("1")), *expr); + assert_eq!(Expr::value(number("1")), *expr); assert_eq!(Some(DataType::Int(None)), data_type); assert!(charset.is_none()); assert!(target_before_value); assert_eq!( vec![ - Expr::Value(number("2")), - Expr::Value(number("3")), - Expr::Value(Value::Null), + Expr::value(number("2")), + Expr::value(number("3")), + Expr::Value((Value::Null).with_empty_span()), ], styles ); @@ -1080,13 +1362,19 @@ fn parse_substring_in_select() { quote_style: None, span: Span::empty(), })), - substring_from: Some(Box::new(Expr::Value(number("0")))), - substring_for: Some(Box::new(Expr::Value(number("1")))), + substring_from: Some(Box::new(Expr::Value( + (number("0")).with_empty_span() + ))), + substring_for: Some(Box::new(Expr::Value( + (number("1")).with_empty_span() + ))), special: true, + shorthand: false, })], + exclude: None, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "test".to_string(), quote_style: None, span: Span::empty(), @@ -1106,16 +1394,16 @@ fn parse_substring_in_select() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), query ); @@ -1169,9 +1457,9 @@ fn parse_mssql_declare() { span: Span::empty(), }], data_type: Some(Text), - assignment: Some(MsSqlAssignment(Box::new(Expr::Value(SingleQuotedString( - "foobar".to_string() - ))))), + assignment: Some(MsSqlAssignment(Box::new(Expr::Value( + (SingleQuotedString("foobar".to_string())).with_empty_span() + )))), declare_type: None, binary: None, sensitive: None, @@ -1201,23 +1489,25 @@ fn parse_mssql_declare() { for_query: None }] }, - Statement::SetVariable { - local: false, + Statement::Set(Set::SingleAssignment { + scope: None, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("@bar")])), - value: vec![Expr::Value(Value::Number("2".parse().unwrap(), false))], - }, + variable: ObjectName::from(vec![Ident::new("@bar")]), + values: vec![Expr::Value( + (Value::Number("2".parse().unwrap(), false)).with_empty_span() + )], + }), Statement::Query(Box::new(Query { with: None, - limit: None, - limit_by: vec![], - offset: 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, @@ -1226,8 +1516,11 @@ fn parse_mssql_declare() { projection: vec![SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("@bar"))), op: BinaryOperator::Multiply, - right: Box::new(Expr::Value(Value::Number("4".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("4".parse().unwrap(), false)).with_empty_span() + )), })], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1243,11 +1536,132 @@ fn parse_mssql_declare() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))) })) ], ast ); + + let declare_cursor_for_select = + "DECLARE vend_cursor CURSOR FOR SELECT * FROM Purchasing.Vendor"; + 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)"#; + let s = ms().verified_stmt(sql); + assert_eq!( + s, + Statement::RaisError { + message: Box::new(Expr::Value( + (Value::SingleQuotedString("This is a test".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("1".parse().unwrap(), false)).with_empty_span() + )), + arguments: vec![], + options: vec![], + } + ); + + let sql = r#"RAISERROR('This is a test', 16, 1) WITH NOWAIT"#; + let _ = ms().verified_stmt(sql); + + let sql = r#"RAISERROR('This is a test', 16, 1, 'ARG') WITH SETERROR, LOG"#; + let _ = ms().verified_stmt(sql); + + let sql = r#"RAISERROR(N'This is message %s %d.', 10, 1, N'number', 5)"#; + let _ = ms().verified_stmt(sql); + + let sql = r#"RAISERROR(N'<<%*.*s>>', 10, 1, 7, 3, N'abcde')"#; + let _ = ms().verified_stmt(sql); + + let sql = r#"RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState)"#; + let _ = ms().verified_stmt(sql); } #[test] @@ -1264,16 +1678,16 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - ms().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + ms().verified_stmt(&format!("USE {object_name}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - ms().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_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(), )]))) @@ -1303,7 +1717,7 @@ fn parse_create_table_with_valid_options() { SqlOption::Partition { column_name: "column_a".into(), range_direction: None, - for_values: vec![Expr::Value(test_utils::number("10")), Expr::Value(test_utils::number("11"))] , + for_values: vec![Expr::Value((test_utils::number("10")).with_empty_span()), Expr::Value((test_utils::number("11")).with_empty_span())] , }, ], ), @@ -1314,8 +1728,8 @@ fn parse_create_table_with_valid_options() { column_name: "column_a".into(), range_direction: Some(PartitionRangeDirection::Left), for_values: vec![ - Expr::Value(test_utils::number("10")), - Expr::Value(test_utils::number("11")), + Expr::Value((test_utils::number("10")).with_empty_span()), + Expr::Value((test_utils::number("11")).with_empty_span()), ], } ], @@ -1375,7 +1789,7 @@ fn parse_create_table_with_valid_options() { }, value: Expr::Function( Function { - name: ObjectName( + name: ObjectName::from( vec![ Ident { value: "HASH".to_string(), @@ -1436,10 +1850,11 @@ fn parse_create_table_with_valid_options() { temporary: false, external: false, global: None, + dynamic: false, if_not_exists: false, transient: false, volatile: false, - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "mytable".to_string(), quote_style: None, span: Span::empty(), @@ -1452,7 +1867,6 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - collation: None, options: vec![], }, ColumnDef { @@ -1462,7 +1876,6 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - collation: None, options: vec![], }, ColumnDef { @@ -1472,7 +1885,6 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - collation: None, options: vec![], }, ], @@ -1484,19 +1896,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, @@ -1504,8 +1910,9 @@ fn parse_create_table_with_valid_options() { partition_by: None, cluster_by: None, clustered_by: None, - options: None, + inherits: None, strict: false, + iceberg: false, copy_grants: false, enable_schema_evolution: None, change_tracking: None, @@ -1515,11 +1922,39 @@ fn parse_create_table_with_valid_options() { with_aggregation_policy: None, with_row_access_policy: None, with_tags: None, + base_location: None, + external_volume: None, + 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, }) ); } } +#[test] +fn parse_nested_slash_star_comment() { + let sql = r#" + select + /* + comment level 1 + /* + comment level 2 + */ + */ + 1; + "#; + let canonical = "SELECT 1"; + ms().one_statement_parses_to(sql, canonical); +} + #[test] fn parse_create_table_with_invalid_options() { let invalid_cases = vec![ @@ -1581,8 +2016,8 @@ fn parse_create_table_with_identity_column() { IdentityProperty { parameters: Some(IdentityPropertyFormatKind::FunctionCall( IdentityParameters { - seed: Expr::Value(number("1")), - increment: Expr::Value(number("1")), + seed: Expr::value(number("1")), + increment: Expr::value(number("1")), }, )), order: None, @@ -1605,10 +2040,12 @@ fn parse_create_table_with_identity_column() { temporary: false, external: false, global: None, + dynamic: false, if_not_exists: false, transient: false, volatile: false, - name: ObjectName(vec![Ident { + iceberg: false, + name: ObjectName::from(vec![Ident { value: "mytable".to_string(), quote_style: None, span: Span::empty(), @@ -1620,7 +2057,7 @@ fn parse_create_table_with_identity_column() { span: Span::empty(), }, data_type: Int(None,), - collation: None, + options: column_options, },], constraints: vec![], @@ -1631,19 +2068,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, @@ -1651,7 +2082,7 @@ 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, enable_schema_evolution: None, @@ -1662,6 +2093,18 @@ fn parse_create_table_with_identity_column() { with_aggregation_policy: None, with_row_access_policy: None, with_tags: None, + base_location: None, + external_volume: None, + 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, }), ); } @@ -1740,9 +2183,345 @@ fn parse_mssql_set_session_value() { ms().verified_stmt("SET ANSI_NULLS, ANSI_PADDING ON"); } +#[test] +fn parse_mssql_if_else() { + // Simple statements and blocks + ms().verified_stmt("IF 1 = 1 SELECT '1'; ELSE SELECT '2';"); + ms().verified_stmt("IF 1 = 1 BEGIN SET @A = 1; END ELSE SET @A = 2;"); + ms().verified_stmt( + "IF DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday') SELECT 'Weekend'; ELSE SELECT 'Weekday';" + ); + ms().verified_stmt( + "IF (SELECT COUNT(*) FROM a.b WHERE c LIKE 'x%') > 1 SELECT 'yes'; ELSE SELECT 'No';", + ); + + // Multiple statements + let stmts = ms() + .parse_sql_statements("DECLARE @A INT; IF 1=1 BEGIN SET @A = 1 END ELSE SET @A = 2") + .unwrap(); + match &stmts[..] { + [Statement::Declare { .. }, Statement::If(stmt)] => { + assert_eq!( + stmt.to_string(), + "IF 1 = 1 BEGIN SET @A = 1; END ELSE SET @A = 2;" + ); + } + _ => panic!("Unexpected statements: {stmts:?}"), + } +} + +#[test] +fn test_mssql_if_else_span() { + let sql = "IF 1 = 1 SELECT '1' ELSE SELECT '2'"; + let mut parser = Parser::new(&MsSqlDialect {}).try_with_sql(sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new(Location::new(1, 1), Location::new(1, sql.len() as u64 + 1)) + ); +} + +#[test] +fn test_mssql_if_else_multiline_span() { + let sql_line1 = "IF 1 = 1"; + let sql_line2 = "SELECT '1'"; + let sql_line3 = "ELSE SELECT '2'"; + let sql = [sql_line1, sql_line2, sql_line3].join("\n"); + let mut parser = Parser::new(&MsSqlDialect {}).try_with_sql(&sql).unwrap(); + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new( + Location::new(1, 1), + Location::new(3, sql_line3.len() as u64 + 1) + ) + ); +} + +#[test] +fn test_mssql_if_statements_span() { + // Simple statements + let mut sql = "IF 1 = 1 SELECT '1' ELSE SELECT '2'"; + let mut parser = Parser::new(&MsSqlDialect {}).try_with_sql(sql).unwrap(); + match parser.parse_statement().unwrap() { + Statement::If(IfStatement { + if_block, + else_block: Some(else_block), + .. + }) => { + assert_eq!( + if_block.span(), + Span::new(Location::new(1, 1), Location::new(1, 20)) + ); + assert_eq!( + else_block.span(), + Span::new(Location::new(1, 21), Location::new(1, 36)) + ); + } + stmt => panic!("Unexpected statement: {stmt:?}"), + } + + // Blocks + sql = "IF 1 = 1 BEGIN SET @A = 1; END ELSE BEGIN SET @A = 2 END"; + parser = Parser::new(&MsSqlDialect {}).try_with_sql(sql).unwrap(); + match parser.parse_statement().unwrap() { + Statement::If(IfStatement { + if_block, + else_block: Some(else_block), + .. + }) => { + assert_eq!( + if_block.span(), + Span::new(Location::new(1, 1), Location::new(1, 31)) + ); + assert_eq!( + else_block.span(), + Span::new(Location::new(1, 32), Location::new(1, 57)) + ); + } + stmt => panic!("Unexpected statement: {stmt:?}"), + } +} + +#[test] +fn parse_mssql_varbinary_max_length() { + let sql = "CREATE TABLE example (var_binary_col VARBINARY(MAX))"; + + match ms_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { name, columns, .. }) => { + assert_eq!( + name, + ObjectName::from(vec![Ident { + value: "example".to_string(), + quote_style: None, + span: Span::empty(), + }]) + ); + assert_eq!( + columns, + vec![ColumnDef { + name: Ident::new("var_binary_col"), + data_type: Varbinary(Some(BinaryLength::Max)), + + options: vec![] + },], + ); + } + _ => unreachable!(), + } + + let sql = "CREATE TABLE example (var_binary_col VARBINARY(50))"; + + match ms_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { name, columns, .. }) => { + assert_eq!( + name, + ObjectName::from(vec![Ident { + value: "example".to_string(), + quote_style: None, + span: Span::empty(), + }]) + ); + assert_eq!( + columns, + vec![ColumnDef { + name: Ident::new("var_binary_col"), + data_type: Varbinary(Some(BinaryLength::IntegerLength { length: 50 })), + + options: vec![] + },], + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_mssql_table_identifier_with_default_schema() { + ms().verified_stmt("SELECT * FROM mydatabase..MyTable"); +} + 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 {})]) } + +#[test] +fn parse_mssql_merge_with_output() { + let stmt = "MERGE dso.products AS t \ + USING dsi.products AS \ + s ON s.ProductID = t.ProductID \ + WHEN MATCHED AND \ + NOT (t.ProductName = s.ProductName OR (ISNULL(t.ProductName, s.ProductName) IS NULL)) \ + THEN UPDATE SET t.ProductName = s.ProductName \ + WHEN NOT MATCHED BY TARGET \ + THEN INSERT (ProductID, ProductName) \ + VALUES (s.ProductID, s.ProductName) \ + WHEN NOT MATCHED BY SOURCE THEN DELETE \ + OUTPUT $action, deleted.ProductID INTO dsi.temp_products"; + 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(CreateTrigger { + or_alter: true, + temporary: false, + or_replace: false, + is_constraint: false, + name: ObjectName::from(vec![Ident::new("reminder1")]), + period: Some(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: None, + 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;"; + let drop_stmt = ms().one_statement_parses_to(sql_drop_trigger, ""); + assert_eq!( + drop_stmt, + Statement::DropTrigger(DropTrigger { + if_exists: false, + trigger_name: ObjectName::from(vec![Ident::new("emp_stamp")]), + table_name: None, + option: None, + }) + ); +} + +#[test] +fn parse_print() { + let print_string_literal = "PRINT 'Hello, world!'"; + let print_stmt = ms().verified_stmt(print_string_literal); + assert_eq!( + print_stmt, + Statement::Print(PrintStatement { + message: Box::new(Expr::Value( + (Value::SingleQuotedString("Hello, world!".to_string())).with_empty_span() + )), + }) + ); + + 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 dcf3f57fe..b31a5b7c0 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -52,11 +52,11 @@ fn parse_literal_string() { let select = mysql().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedString("single".to_string())), + &Expr::Value((Value::SingleQuotedString("single".to_string())).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedString("double".to_string())), + &Expr::Value((Value::DoubleQuotedString("double".to_string())).with_empty_span()), expr_from_projection(&select.projection[1]) ); } @@ -149,7 +149,7 @@ fn parse_flush() { read_lock: false, export: false, tables: vec![ - ObjectName(vec![ + ObjectName::from(vec![ Ident { value: "mek".to_string(), quote_style: Some('`'), @@ -161,7 +161,7 @@ fn parse_flush() { span: Span::empty(), } ]), - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "table2".to_string(), quote_style: None, span: Span::empty(), @@ -189,7 +189,7 @@ fn parse_flush() { read_lock: true, export: false, tables: vec![ - ObjectName(vec![ + ObjectName::from(vec![ Ident { value: "mek".to_string(), quote_style: Some('`'), @@ -201,7 +201,7 @@ fn parse_flush() { span: Span::empty(), } ]), - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "table2".to_string(), quote_style: None, span: Span::empty(), @@ -218,7 +218,7 @@ fn parse_flush() { read_lock: false, export: true, tables: vec![ - ObjectName(vec![ + ObjectName::from(vec![ Ident { value: "mek".to_string(), quote_style: Some('`'), @@ -230,7 +230,7 @@ fn parse_flush() { span: Span::empty(), } ]), - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "table2".to_string(), quote_style: None, span: Span::empty(), @@ -251,7 +251,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: None, limit_from: None, @@ -269,7 +269,10 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mydb"), Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![ + Ident::new("mydb"), + Ident::new("mytable") + ])), }), filter_position: None, limit_from: None, @@ -287,7 +290,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: None, limit_from: None, @@ -305,7 +308,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: None, limit_from: None, @@ -323,7 +326,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: Some(ShowStatementFilterPosition::Suffix( ShowStatementFilter::Like("pattern".into()) @@ -343,7 +346,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: Some(ShowStatementFilterPosition::Suffix( ShowStatementFilter::Where(mysql_and_generic().verified_expr("1 = 2")) @@ -430,7 +433,7 @@ fn parse_show_tables() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mydb")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mydb")])), }), filter_position: None } @@ -534,7 +537,7 @@ fn parse_show_extended_full() { #[test] fn parse_show_create() { - let obj_name = ObjectName(vec![Ident::new("myident")]); + let obj_name = ObjectName::from(vec![Ident::new("myident")]); for obj_type in &[ ShowCreateObject::Table, @@ -590,17 +593,16 @@ 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)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + mysql_and_generic().verified_stmt(&format!("USE {object_name}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); 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)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_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(), )]))) @@ -614,12 +616,12 @@ fn parse_set_variables() { mysql_and_generic().verified_stmt("SET sql_mode = CONCAT(@@sql_mode, ',STRICT_TRANS_TABLES')"); assert_eq!( mysql_and_generic().verified_stmt("SET LOCAL autocommit = 1"), - Statement::SetVariable { - local: true, + Statement::Set(Set::SingleAssignment { + scope: Some(ContextModifier::Local), hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec!["autocommit".into()])), - value: vec![Expr::Value(number("1"))], - } + variable: ObjectName::from(vec!["autocommit".into()]), + values: vec![Expr::value(number("1"))], + }) ); } @@ -633,14 +635,17 @@ fn parse_create_table_auto_increment() { vec![ColumnDef { name: Ident::new("bar"), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None - }, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + }), }, ColumnOptionDef { name: None, @@ -668,8 +673,22 @@ 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 { + Some(index_type_display) => UniqueConstraint { name, index_name, index_type_display, @@ -678,15 +697,17 @@ fn table_constraint_unique_primary_ctor( index_options, characteristics, nulls_distinct: NullsDistinctOption::None, - }, - None => TableConstraint::PrimaryKey { + } + .into(), + None => PrimaryKeyConstraint { name, index_name, index_type, columns, index_options, characteristics, - }, + } + .into(), } } @@ -723,14 +744,17 @@ fn parse_create_table_primary_and_unique_key() { ColumnDef { name: Ident::new("id"), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None - }, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + }), }, ColumnOptionDef { name: None, @@ -743,7 +767,6 @@ fn parse_create_table_primary_and_unique_key() { ColumnDef { name: Ident::new("bar"), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -795,6 +818,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 +932,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 +957,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"); - for sql in [canonical, with_equal] { - match mysql().one_statement_parses_to(sql, canonical) { + let plain_options = match table_options { + CreateTableOptions::Plain(options) => options, + _ => unreachable!(), + }; + + 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'))"; @@ -893,7 +1188,6 @@ fn parse_create_table_set_enum() { ColumnDef { name: Ident::new("bar"), data_type: DataType::Set(vec!["a".to_string(), "b".to_string()]), - collation: None, options: vec![], }, ColumnDef { @@ -905,7 +1199,6 @@ fn parse_create_table_set_enum() { ], None ), - collation: None, options: vec![], } ], @@ -918,13 +1211,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"); @@ -932,19 +1224,28 @@ fn parse_create_table_engine_default_charset() { vec![ColumnDef { name: Ident::new("id"), data_type: DataType::Int(Some(11)), - collation: None, options: vec![], },], 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!(), } @@ -952,12 +1253,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"); @@ -965,12 +1266,20 @@ fn parse_create_table_collate() { vec![ColumnDef { name: Ident::new("id"), data_type: DataType::Int(Some(11)), - collation: None, options: vec![], },], 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!(), } @@ -978,25 +1287,38 @@ 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(number("1")))] + vec![SelectItem::UnnamedExpr(Expr::Value( + (number("1")).with_empty_span() + ))] ); } _ => 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(_)) @@ -1013,11 +1335,10 @@ fn parse_create_table_comment_character_set() { vec![ColumnDef { name: Ident::new("s"), data_type: DataType::Text, - collation: None, options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::CharacterSet(ObjectName(vec![Ident::new( + option: ColumnOption::CharacterSet(ObjectName::from(vec![Ident::new( "utf8mb4" )])) }, @@ -1050,6 +1371,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)"; @@ -1060,13 +1388,16 @@ fn parse_quote_identifiers() { vec![ColumnDef { name: Ident::with_quote('`', "BEGIN"), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None - }, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + }), }], }], columns @@ -1093,6 +1424,7 @@ fn parse_escaped_quote_identifiers_with_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1108,16 +1440,16 @@ fn parse_escaped_quote_identifiers_with_escape() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1131,6 +1463,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { ParserOptions { trailing_commas: false, unescape: false, + require_semicolon_stmt_delimiter: true, } ) .verified_stmt(sql), @@ -1146,6 +1479,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1161,16 +1495,16 @@ fn parse_escaped_quote_identifiers_with_no_escape() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1193,6 +1527,7 @@ fn parse_escaped_backticks_with_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1208,16 +1543,16 @@ fn parse_escaped_backticks_with_escape() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1244,6 +1579,7 @@ fn parse_escaped_backticks_with_no_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1259,16 +1595,16 @@ fn parse_escaped_backticks_with_no_escape() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ); } @@ -1319,31 +1655,26 @@ fn parse_create_table_with_minimum_display_width() { ColumnDef { name: Ident::new("bar_tinyint"), data_type: DataType::TinyInt(Some(3)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_smallint"), data_type: DataType::SmallInt(Some(5)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_mediumint"), data_type: DataType::MediumInt(Some(6)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_int"), data_type: DataType::Int(Some(11)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_bigint"), data_type: DataType::BigInt(Some(20)), - collation: None, options: vec![], } ], @@ -1364,32 +1695,164 @@ fn parse_create_table_unsigned() { vec![ ColumnDef { name: Ident::new("bar_tinyint"), - data_type: DataType::UnsignedTinyInt(Some(3)), - collation: None, + data_type: DataType::TinyIntUnsigned(Some(3)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_smallint"), + data_type: DataType::SmallIntUnsigned(Some(5)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_mediumint"), + data_type: DataType::MediumIntUnsigned(Some(13)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_int"), + data_type: DataType::IntUnsigned(Some(11)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_bigint"), + data_type: DataType::BigIntUnsigned(Some(20)), + options: vec![], + }, + ], + columns + ); + } + _ => unreachable!(), + } +} + +#[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::UnsignedSmallInt(Some(5)), - collation: None, + data_type: DataType::SmallInt(Some(5)), options: vec![], }, ColumnDef { name: Ident::new("bar_mediumint"), - data_type: DataType::UnsignedMediumInt(Some(13)), - collation: None, + data_type: DataType::MediumInt(Some(13)), options: vec![], }, ColumnDef { name: Ident::new("bar_int"), - data_type: DataType::UnsignedInt(Some(11)), - collation: None, + data_type: DataType::Int(Some(11)), options: vec![], }, ColumnDef { name: Ident::new("bar_bigint"), - data_type: DataType::UnsignedBigInt(Some(20)), - collation: None, + 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_deprecated_mysql_unsigned_data_types() { + let sql = "CREATE TABLE foo (bar_decimal DECIMAL UNSIGNED, bar_decimal_prec DECIMAL(10) UNSIGNED, bar_decimal_scale DECIMAL(10,2) UNSIGNED, bar_dec DEC UNSIGNED, bar_dec_prec DEC(10) UNSIGNED, bar_dec_scale DEC(10,2) UNSIGNED, bar_float FLOAT UNSIGNED, bar_float_prec FLOAT(10) UNSIGNED, bar_float_scale FLOAT(10,2) UNSIGNED, bar_double DOUBLE UNSIGNED, bar_double_prec DOUBLE(10) UNSIGNED, bar_double_scale DOUBLE(10,2) UNSIGNED, bar_real REAL UNSIGNED, bar_double_precision DOUBLE PRECISION UNSIGNED)"; + match mysql().verified_stmt(sql) { + Statement::CreateTable(CreateTable { name, columns, .. }) => { + assert_eq!(name.to_string(), "foo"); + assert_eq!( + vec![ + ColumnDef { + name: Ident::new("bar_decimal"), + data_type: DataType::DecimalUnsigned(ExactNumberInfo::None), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_decimal_prec"), + data_type: DataType::DecimalUnsigned(ExactNumberInfo::Precision(10)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_decimal_scale"), + data_type: DataType::DecimalUnsigned(ExactNumberInfo::PrecisionAndScale( + 10, 2 + )), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_dec"), + data_type: DataType::DecUnsigned(ExactNumberInfo::None), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_dec_prec"), + data_type: DataType::DecUnsigned(ExactNumberInfo::Precision(10)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_dec_scale"), + data_type: DataType::DecUnsigned(ExactNumberInfo::PrecisionAndScale(10, 2)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_float"), + data_type: DataType::FloatUnsigned(ExactNumberInfo::None), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_float_prec"), + data_type: DataType::FloatUnsigned(ExactNumberInfo::Precision(10)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_float_scale"), + data_type: DataType::FloatUnsigned(ExactNumberInfo::PrecisionAndScale( + 10, 2 + )), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_double"), + data_type: DataType::DoubleUnsigned(ExactNumberInfo::None), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_double_prec"), + data_type: DataType::DoubleUnsigned(ExactNumberInfo::Precision(10)), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_double_scale"), + data_type: DataType::DoubleUnsigned(ExactNumberInfo::PrecisionAndScale( + 10, 2 + )), + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_real"), + data_type: DataType::RealUnsigned, + options: vec![], + }, + ColumnDef { + name: Ident::new("bar_double_precision"), + data_type: DataType::DoublePrecisionUnsigned, options: vec![], }, ], @@ -1413,7 +1876,7 @@ fn parse_simple_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1422,33 +1885,40 @@ fn parse_simple_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![ vec![ - Expr::Value(Value::SingleQuotedString( - "Test Some Inserts".to_string() - )), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ], vec![ - Expr::Value(Value::SingleQuotedString("Test Entry 2".to_string())), - Expr::Value(number("2")) + Expr::Value( + (Value::SingleQuotedString("Test Entry 2".to_string())) + .with_empty_span() + ), + Expr::value(number("2")) ], vec![ - Expr::Value(Value::SingleQuotedString("Test Entry 3".to_string())), - Expr::Value(number("3")) + Expr::Value( + (Value::SingleQuotedString("Test Entry 3".to_string())) + .with_empty_span() + ), + Expr::value(number("3")) ] ] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1471,7 +1941,7 @@ fn parse_ignore_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1481,21 +1951,24 @@ fn parse_ignore_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1518,7 +1991,7 @@ fn parse_priority_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1528,21 +2001,24 @@ fn parse_priority_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1562,7 +2038,7 @@ fn parse_priority_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1572,21 +2048,24 @@ fn parse_priority_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1607,14 +2086,14 @@ fn parse_insert_as() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::with_quote('`', "table")])), + TableObject::TableName(ObjectName::from(vec![Ident::with_quote('`', "table")])), table_name ); assert_eq!(vec![Ident::with_quote('`', "date")], columns); let insert_alias = insert_alias.unwrap(); assert_eq!( - ObjectName(vec![Ident::with_quote('`', "alias")]), + ObjectName::from(vec![Ident::with_quote('`', "alias")]), insert_alias.row_alias ); assert_eq!(Some(vec![]), insert_alias.col_aliases); @@ -1622,20 +2101,20 @@ fn parse_insert_as() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, - rows: vec![vec![Expr::Value(Value::SingleQuotedString( - "2024-01-01".to_string() - ))]] + rows: vec![vec![Expr::Value( + (Value::SingleQuotedString("2024-01-01".to_string())).with_empty_span() + )]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1659,7 +2138,7 @@ fn parse_insert_as() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::with_quote('`', "table")])), + TableObject::TableName(ObjectName::from(vec![Ident::with_quote('`', "table")])), table_name ); assert_eq!( @@ -1668,7 +2147,7 @@ fn parse_insert_as() { ); let insert_alias = insert_alias.unwrap(); assert_eq!( - ObjectName(vec![Ident::with_quote('`', "alias")]), + ObjectName::from(vec![Ident::with_quote('`', "alias")]), insert_alias.row_alias ); assert_eq!( @@ -1682,21 +2161,24 @@ fn parse_insert_as() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ - Expr::Value(number("1")), - Expr::Value(Value::SingleQuotedString("2024-01-01".to_string())) + Expr::value(number("1")), + Expr::Value( + (Value::SingleQuotedString("2024-01-01".to_string())) + .with_empty_span() + ) ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1719,7 +2201,7 @@ fn parse_replace_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1730,21 +2212,24 @@ fn parse_replace_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1766,7 +2251,7 @@ fn parse_empty_row_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tb")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tb")])), table_name ); assert!(columns.is_empty()); @@ -1775,18 +2260,18 @@ fn parse_empty_row_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![], vec![]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); @@ -1808,7 +2293,7 @@ fn parse_insert_with_on_duplicate_update() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("permission_groups")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("permission_groups")])), table_name ); assert_eq!( @@ -1826,60 +2311,64 @@ fn parse_insert_with_on_duplicate_update() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString( - "accounting_manager".to_string() - )), - Expr::Value(Value::SingleQuotedString( - "Some description about the group".to_string() - )), - Expr::Value(Value::Boolean(true)), - Expr::Value(Value::Boolean(true)), - Expr::Value(Value::Boolean(true)), - Expr::Value(Value::Boolean(true)), + Expr::Value( + (Value::SingleQuotedString("accounting_manager".to_string())) + .with_empty_span() + ), + Expr::Value( + (Value::SingleQuotedString( + "Some description about the group".to_string() + )) + .with_empty_span() + ), + Expr::Value((Value::Boolean(true)).with_empty_span()), + Expr::Value((Value::Boolean(true)).with_empty_span()), + Expr::Value((Value::Boolean(true)).with_empty_span()), + Expr::Value((Value::Boolean(true)).with_empty_span()), ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), source ); assert_eq!( Some(OnInsert::DuplicateKeyUpdate(vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "description".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("description"))]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "perm_create".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_create"))]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "perm_read".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_read"))]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "perm_update".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_update"))]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "perm_delete".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_delete"))]), @@ -1908,9 +2397,10 @@ 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(vec![Ident::with_quote( + relation: table_from_name(ObjectName::from(vec![Ident::with_quote( '"', "table" )])), joins: vec![] @@ -1928,6 +2418,7 @@ fn parse_select_with_numeric_prefix_column_name() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))) ); } @@ -1935,6 +2426,128 @@ fn parse_select_with_numeric_prefix_column_name() { } } +#[test] +fn parse_qualified_identifiers_with_numeric_prefix() { + // Case 1: Qualified column name that starts with digits. + match mysql().verified_stmt("SELECT t.15to29 FROM my_table AS t") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!(&[Ident::new("t"), Ident::new("15to29")], &parts[..]); + } + proj => panic!("Unexpected projection: {proj:?}"), + }, + body => panic!("Unexpected statement body: {body:?}"), + }, + stmt => panic!("Unexpected statement: {stmt:?}"), + } + + // Case 2: Qualified column name that starts with digits and on its own represents a number. + match mysql().verified_stmt("SELECT t.15e29 FROM my_table AS t") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!(&[Ident::new("t"), Ident::new("15e29")], &parts[..]); + } + proj => panic!("Unexpected projection: {proj:?}"), + }, + body => panic!("Unexpected statement body: {body:?}"), + }, + stmt => panic!("Unexpected statement: {stmt:?}"), + } + + // Case 3: Unqualified, the same token is parsed as a number. + match mysql() + .parse_sql_statements("SELECT 15e29 FROM my_table") + .unwrap() + .pop() + { + Some(Statement::Query(q)) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan { value, .. }))) => { + assert_eq!(&number("15e29"), value); + } + proj => panic!("Unexpected projection: {proj:?}"), + }, + body => panic!("Unexpected statement body: {body:?}"), + }, + stmt => panic!("Unexpected statement: {stmt:?}"), + } + + // Case 4: Quoted simple identifier. + match mysql().verified_stmt("SELECT `15e29` FROM my_table") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::Identifier(name))) => { + assert_eq!(&Ident::with_quote('`', "15e29"), name); + } + proj => panic!("Unexpected projection: {proj:?}"), + }, + body => panic!("Unexpected statement body: {body:?}"), + }, + stmt => panic!("Unexpected statement: {stmt:?}"), + } + + // Case 5: Quoted compound identifier. + match mysql().verified_stmt("SELECT t.`15e29` FROM my_table AS t") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!( + &[Ident::new("t"), Ident::with_quote('`', "15e29")], + &parts[..] + ); + } + proj => panic!("Unexpected projection: {proj:?}"), + }, + body => panic!("Unexpected statement body: {body:?}"), + }, + stmt => panic!("Unexpected statement: {stmt:?}"), + } + + // Case 6: Multi-level compound identifiers. + match mysql().verified_stmt("SELECT 1db.1table.1column") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!( + &[ + Ident::new("1db"), + Ident::new("1table"), + Ident::new("1column") + ], + &parts[..] + ); + } + proj => panic!("Unexpected projection: {proj:?}"), + }, + body => panic!("Unexpected statement body: {body:?}"), + }, + stmt => panic!("Unexpected statement: {stmt:?}"), + } + + // Case 7: Multi-level compound quoted identifiers. + match mysql().verified_stmt("SELECT `1`.`2`.`3`") { + Statement::Query(q) => match *q.body { + SetExpr::Select(s) => match s.projection.last() { + Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { + assert_eq!( + &[ + Ident::with_quote('`', "1"), + Ident::with_quote('`', "2"), + Ident::with_quote('`', "3") + ], + &parts[..] + ); + } + proj => panic!("Unexpected projection: {proj:?}"), + }, + body => panic!("Unexpected statement body: {body:?}"), + }, + stmt => panic!("Unexpected statement: {stmt:?}"), + } +} + // Don't run with bigdecimal as it fails like this on rust beta: // // 'parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column' @@ -1952,17 +2565,17 @@ 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, projection: vec![ - SelectItem::UnnamedExpr(Expr::Value(number("123e4"))), + 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(vec![Ident::with_quote( + relation: table_from_name(ObjectName::from(vec![Ident::with_quote( '"', "table" )])), joins: vec![] @@ -1980,6 +2593,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))) ); } @@ -1997,7 +2611,7 @@ fn parse_insert_with_numeric_prefix_column_name() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("s1"), Ident::new("t1")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("s1"), Ident::new("t1")])), table_name ); assert_eq!(vec![Ident::new("123col_$@length123")], columns); @@ -2010,18 +2624,19 @@ fn parse_insert_with_numeric_prefix_column_name() { fn parse_update_with_joins() { let sql = "UPDATE orders AS o JOIN customers AS c ON o.customer_id = c.id SET o.completed = true WHERE c.firstname = 'Peter'"; match mysql().verified_stmt(sql) { - Statement::Update { + Statement::Update(Update { table, assignments, from: _from, selection, returning, or: None, - } => { + limit: None, + }) => { assert_eq!( TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("orders")]), + name: ObjectName::from(vec![Ident::new("orders")]), alias: Some(TableAlias { name: Ident::new("o"), columns: vec![] @@ -2033,10 +2648,11 @@ fn parse_update_with_joins() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![Join { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("customers")]), + name: ObjectName::from(vec![Ident::new("customers")]), alias: Some(TableAlias { name: Ident::new("c"), columns: vec![] @@ -2048,9 +2664,10 @@ fn parse_update_with_joins() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, global: false, - join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("o"), Ident::new("customer_id") @@ -2067,11 +2684,11 @@ fn parse_update_with_joins() { ); assert_eq!( vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![ + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ Ident::new("o"), Ident::new("completed") ])), - value: Expr::Value(Value::Boolean(true)) + value: Expr::Value((Value::Boolean(true)).with_empty_span()) }], assignments ); @@ -2082,7 +2699,9 @@ fn parse_update_with_joins() { Ident::new("firstname") ])), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString("Peter".to_string()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("Peter".to_string())).with_empty_span() + )) }), selection ); @@ -2104,8 +2723,10 @@ fn parse_delete_with_order_by() { quote_style: None, span: Span::empty(), }), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }], order_by @@ -2120,7 +2741,7 @@ fn parse_delete_with_limit() { let sql = "DELETE FROM customers LIMIT 100"; match mysql().verified_stmt(sql) { Statement::Delete(Delete { limit, .. }) => { - assert_eq!(Some(Expr::Value(number("100"))), limit); + assert_eq!(Some(Expr::value(number("100"))), limit); } _ => unreachable!(), } @@ -2129,16 +2750,19 @@ fn parse_delete_with_limit() { #[test] fn parse_alter_table_add_column() { match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT FIRST") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, + table_type, location: _, on_cluster: _, - } => { + end_token: _, + }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); + assert_eq!(table_type, None); assert!(!only); assert_eq!( operations, @@ -2148,7 +2772,6 @@ fn parse_alter_table_add_column() { column_def: ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![], }, column_position: Some(MySQLColumnPosition::First), @@ -2159,14 +2782,13 @@ fn parse_alter_table_add_column() { } match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT AFTER foo") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, - location: _, - on_cluster: _, - } => { + .. + }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); assert!(!only); @@ -2178,7 +2800,6 @@ fn parse_alter_table_add_column() { column_def: ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![], }, column_position: Some(MySQLColumnPosition::After(Ident { @@ -2198,14 +2819,13 @@ fn parse_alter_table_add_columns() { match mysql() .verified_stmt("ALTER TABLE tab ADD COLUMN a TEXT FIRST, ADD COLUMN b INT AFTER foo") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, - location: _, - on_cluster: _, - } => { + .. + }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); assert!(!only); @@ -2218,7 +2838,6 @@ fn parse_alter_table_add_columns() { column_def: ColumnDef { name: "a".into(), data_type: DataType::Text, - collation: None, options: vec![], }, column_position: Some(MySQLColumnPosition::First), @@ -2229,7 +2848,6 @@ fn parse_alter_table_add_columns() { column_def: ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![], }, column_position: Some(MySQLColumnPosition::After(Ident { @@ -2249,13 +2867,25 @@ 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 + } + ); +} + +#[test] +fn parse_alter_table_drop_foreign_key() { + assert_matches!( + 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" ); } #[test] fn parse_alter_table_change_column() { - let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_name = ObjectName::from(vec![Ident::new("orders")]); let expected_operation = AlterTableOperation::ChangeColumn { old_name: Ident::new("description"), new_name: Ident::new("desc"), @@ -2307,7 +2937,7 @@ fn parse_alter_table_change_column() { #[test] fn parse_alter_table_change_column_with_column_position() { - let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_name = ObjectName::from(vec![Ident::new("orders")]); let expected_operation_first = AlterTableOperation::ChangeColumn { old_name: Ident::new("description"), new_name: Ident::new("desc"), @@ -2355,7 +2985,7 @@ fn parse_alter_table_change_column_with_column_position() { #[test] fn parse_alter_table_modify_column() { - let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_name = ObjectName::from(vec![Ident::new("orders")]); let expected_operation = AlterTableOperation::ModifyColumn { col_name: Ident::new("description"), data_type: DataType::Text, @@ -2402,9 +3032,118 @@ fn parse_alter_table_modify_column() { assert_eq!(expected_operation, operation); } +#[test] +fn parse_alter_table_with_algorithm() { + let sql = "ALTER TABLE tab ALGORITHM = COPY"; + let expected_operation = AlterTableOperation::Algorithm { + equals: true, + algorithm: AlterTableAlgorithm::Copy, + }; + let operation = alter_table_op(mysql_and_generic().verified_stmt(sql)); + assert_eq!(expected_operation, operation); + + // Check order doesn't matter + let sql = + "ALTER TABLE users DROP COLUMN password_digest, ALGORITHM = COPY, RENAME COLUMN name TO username"; + let stmt = mysql_and_generic().verified_stmt(sql); + match stmt { + Statement::AlterTable(AlterTable { operations, .. }) => { + assert_eq!( + operations, + vec![ + AlterTableOperation::DropColumn { + has_column_keyword: true, + column_names: vec![Ident::new("password_digest")], + if_exists: false, + drop_behavior: None, + }, + AlterTableOperation::Algorithm { + equals: true, + algorithm: AlterTableAlgorithm::Copy, + }, + AlterTableOperation::RenameColumn { + old_column_name: Ident::new("name"), + new_column_name: Ident::new("username") + }, + ] + ) + } + _ => panic!("Unexpected statement {stmt}"), + } + + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM DEFAULT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM INSTANT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM INPLACE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM COPY"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = DEFAULT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = INSTANT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = INPLACE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = COPY"); +} + +#[test] +fn parse_alter_table_with_lock() { + let sql = "ALTER TABLE tab LOCK = SHARED"; + let expected_operation = AlterTableOperation::Lock { + equals: true, + lock: AlterTableLock::Shared, + }; + let operation = alter_table_op(mysql_and_generic().verified_stmt(sql)); + assert_eq!(expected_operation, operation); + + let sql = + "ALTER TABLE users DROP COLUMN password_digest, LOCK = EXCLUSIVE, RENAME COLUMN name TO username"; + let stmt = mysql_and_generic().verified_stmt(sql); + match stmt { + Statement::AlterTable(AlterTable { operations, .. }) => { + assert_eq!( + operations, + vec![ + AlterTableOperation::DropColumn { + has_column_keyword: true, + column_names: vec![Ident::new("password_digest")], + if_exists: false, + drop_behavior: None, + }, + AlterTableOperation::Lock { + equals: true, + lock: AlterTableLock::Exclusive, + }, + AlterTableOperation::RenameColumn { + old_column_name: Ident::new("name"), + new_column_name: Ident::new("username") + }, + ] + ) + } + _ => panic!("Unexpected statement {stmt}"), + } + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK DEFAULT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK SHARED"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK NONE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK EXCLUSIVE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK = DEFAULT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK = SHARED"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK = NONE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` LOCK = EXCLUSIVE"); +} + +#[test] +fn parse_alter_table_auto_increment() { + let sql = "ALTER TABLE tab AUTO_INCREMENT = 42"; + let expected_operation = AlterTableOperation::AutoIncrement { + equals: true, + value: number("42").with_empty_span(), + }; + let operation = alter_table_op(mysql().verified_stmt(sql)); + assert_eq!(expected_operation, operation); + + mysql_and_generic().verified_stmt("ALTER TABLE `users` AUTO_INCREMENT 42"); +} + #[test] fn parse_alter_table_modify_column_with_column_position() { - let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_name = ObjectName::from(vec![Ident::new("orders")]); let expected_operation_first = AlterTableOperation::ModifyColumn { col_name: Ident::new("description"), data_type: DataType::Text, @@ -2472,13 +3211,19 @@ fn parse_substring_in_select() { quote_style: None, span: Span::empty(), })), - substring_from: Some(Box::new(Expr::Value(number("0")))), - substring_for: Some(Box::new(Expr::Value(number("1")))), + substring_from: Some(Box::new(Expr::Value( + (number("0")).with_empty_span() + ))), + substring_for: Some(Box::new(Expr::Value( + (number("1")).with_empty_span() + ))), special: true, + shorthand: false, })], + exclude: None, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "test".to_string(), quote_style: None, span: Span::empty(), @@ -2498,16 +3243,16 @@ fn parse_substring_in_select() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], }), query ); @@ -2541,6 +3286,17 @@ fn parse_rlike_and_regexp() { } } +#[test] +fn parse_like_with_escape() { + // verify backslash is not stripped for escaped wildcards + mysql().verified_only_select(r#"SELECT 'a\%c' LIKE 'a\%c'"#); + mysql().verified_only_select(r#"SELECT 'a\_c' LIKE 'a\_c'"#); + mysql().verified_only_select(r#"SELECT '%\_\%' LIKE '%\_\%'"#); + mysql().verified_only_select(r#"SELECT '\_\%' LIKE CONCAT('\_', '\%')"#); + mysql().verified_only_select(r#"SELECT 'a%c' LIKE 'a$%c' ESCAPE '$'"#); + mysql().verified_only_select(r#"SELECT 'a_c' LIKE 'a#_c' ESCAPE '#'"#); +} + #[test] fn parse_kill() { let stmt = mysql_and_generic().verified_stmt("KILL CONNECTION 5"); @@ -2572,7 +3328,7 @@ fn parse_kill() { } #[test] -fn parse_table_colum_option_on_update() { +fn parse_table_column_option_on_update() { let sql1 = "CREATE TABLE foo (`modification_time` DATETIME ON UPDATE CURRENT_TIMESTAMP())"; match mysql().verified_stmt(sql1) { Statement::CreateTable(CreateTable { name, columns, .. }) => { @@ -2581,7 +3337,6 @@ fn parse_table_colum_option_on_update() { vec![ColumnDef { name: Ident::with_quote('`', "modification_time"), data_type: DataType::Datetime(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::OnUpdate(call("CURRENT_TIMESTAMP", [])), @@ -2599,19 +3354,19 @@ fn parse_set_names() { let stmt = mysql_and_generic().verified_stmt("SET NAMES utf8mb4"); assert_eq!( stmt, - Statement::SetNames { - charset_name: "utf8mb4".to_string(), + Statement::Set(Set::SetNames { + charset_name: "utf8mb4".into(), collation_name: None, - } + }) ); let stmt = mysql_and_generic().verified_stmt("SET NAMES utf8mb4 COLLATE bogus"); assert_eq!( stmt, - Statement::SetNames { - charset_name: "utf8mb4".to_string(), + Statement::Set(Set::SetNames { + charset_name: "utf8mb4".into(), collation_name: Some("bogus".to_string()), - } + }) ); let stmt = mysql_and_generic() @@ -2619,22 +3374,20 @@ fn parse_set_names() { .unwrap(); assert_eq!( stmt, - vec![Statement::SetNames { - charset_name: "utf8mb4".to_string(), + vec![Statement::Set(Set::SetNames { + charset_name: "utf8mb4".into(), collation_name: Some("bogus".to_string()), - }] + })] ); let stmt = mysql_and_generic().verified_stmt("SET NAMES DEFAULT"); - assert_eq!(stmt, Statement::SetNamesDefault {}); + assert_eq!(stmt, Statement::Set(Set::SetNamesDefault {})); } #[test] fn parse_limit_my_sql_syntax() { - mysql_and_generic().one_statement_parses_to( - "SELECT id, fname, lname FROM customer LIMIT 5, 10", - "SELECT id, fname, lname FROM customer LIMIT 10 OFFSET 5", - ); + mysql_and_generic().verified_stmt("SELECT id, fname, lname FROM customer LIMIT 10 OFFSET 5"); + mysql_and_generic().verified_stmt("SELECT id, fname, lname FROM customer LIMIT 5, 10"); mysql_and_generic().verified_stmt("SELECT * FROM user LIMIT ? OFFSET ?"); } @@ -2775,10 +3528,14 @@ fn parse_hex_string_introducer() { distinct: None, top: None, top_before_distinct: false, - projection: vec![SelectItem::UnnamedExpr(Expr::IntroducedString { - introducer: "_latin1".to_string(), - value: Value::HexStringLiteral("4D7953514C".to_string()) + projection: vec![SelectItem::UnnamedExpr(Expr::Prefixed { + prefix: Ident::from("_latin1"), + value: Expr::Value( + Value::HexStringLiteral("4D7953514C".to_string()).with_empty_span() + ) + .into(), })], + exclude: None, from: vec![], lateral_views: vec![], prewhere: None, @@ -2794,16 +3551,16 @@ fn parse_hex_string_introducer() { value_table_mode: None, into: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })) ) } @@ -2865,19 +3622,27 @@ fn parse_convert_using() { #[test] fn parse_create_table_with_column_collate() { let sql = "CREATE TABLE tb (id TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci)"; - let canonical = "CREATE TABLE tb (id TEXT COLLATE utf8mb4_0900_ai_ci CHARACTER SET utf8mb4)"; - match mysql().one_statement_parses_to(sql, canonical) { + match mysql().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "tb"); assert_eq!( vec![ColumnDef { name: Ident::new("id"), data_type: DataType::Text, - collation: Some(ObjectName(vec![Ident::new("utf8mb4_0900_ai_ci")])), - options: vec![ColumnOptionDef { - name: None, - option: ColumnOption::CharacterSet(ObjectName(vec![Ident::new("utf8mb4")])) - }], + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::CharacterSet(ObjectName::from(vec![Ident::new( + "utf8mb4" + )])) + }, + ColumnOptionDef { + name: None, + option: ColumnOption::Collation(ObjectName::from(vec![Ident::new( + "utf8mb4_0900_ai_ci" + )])) + } + ], },], columns ); @@ -2938,7 +3703,7 @@ fn parse_json_table() { .from[0] .relation, TableFactor::JsonTable { - json_expr: Expr::Value(Value::SingleQuotedString("[1,2]".to_string())), + json_expr: Expr::Value((Value::SingleQuotedString("[1,2]".to_string())).with_empty_span()), json_path: Value::SingleQuotedString("$[*]".to_string()), columns: vec![ JsonTableColumn::Named(JsonTableNamedColumn { @@ -2976,33 +3741,33 @@ fn parse_logical_xor() { let select = mysql_and_generic().verified_only_select(sql); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(true))), + left: Box::new(Expr::Value((Value::Boolean(true)).with_empty_span())), op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(true))), + right: Box::new(Expr::Value((Value::Boolean(true)).with_empty_span())), }), select.projection[0] ); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(false))), + left: Box::new(Expr::Value((Value::Boolean(false)).with_empty_span())), op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(false))), + right: Box::new(Expr::Value((Value::Boolean(false)).with_empty_span())), }), select.projection[1] ); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(true))), + left: Box::new(Expr::Value((Value::Boolean(true)).with_empty_span())), op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(false))), + right: Box::new(Expr::Value((Value::Boolean(false)).with_empty_span())), }), select.projection[2] ); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(false))), + left: Box::new(Expr::Value((Value::Boolean(false)).with_empty_span())), op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(true))), + right: Box::new(Expr::Value((Value::Boolean(true)).with_empty_span())), }), select.projection[3] ); @@ -3014,7 +3779,7 @@ fn parse_bitstring_literal() { assert_eq!( select.projection, vec![SelectItem::UnnamedExpr(Expr::Value( - Value::SingleQuotedByteStringLiteral("111".to_string()) + (Value::SingleQuotedByteStringLiteral("111".to_string())).with_empty_span() ))] ); } @@ -3028,7 +3793,9 @@ fn parse_grant() { objects, grantees, with_grant_option, + as_grantor: _, granted_by, + current_grants: _, } = stmt { assert_eq!( @@ -3039,7 +3806,10 @@ fn parse_grant() { ); assert_eq!( objects, - GrantObjects::Tables(vec![ObjectName(vec!["*".into(), "*".into()])]) + Some(GrantObjects::Tables(vec![ObjectName::from(vec![ + "*".into(), + "*".into() + ])])) ); assert!(!with_grant_option); assert!(granted_by.is_none()); @@ -3080,7 +3850,10 @@ fn parse_revoke() { ); assert_eq!( objects, - GrantObjects::Tables(vec![ObjectName(vec!["db1".into(), "*".into()])]) + Some(GrantObjects::Tables(vec![ObjectName::from(vec![ + "db1".into(), + "*".into() + ])])) ); if let [Grantee { grantee_type: GranteesType::None, @@ -3105,7 +3878,7 @@ fn parse_revoke() { fn parse_create_view_algorithm_param() { let sql = "CREATE ALGORITHM = MERGE VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3113,7 +3886,7 @@ fn parse_create_view_algorithm_param() { security, }), .. - } = stmt + }) = stmt { assert_eq!(algorithm, Some(CreateViewAlgorithm::Merge)); assert!(definer.is_none()); @@ -3129,7 +3902,7 @@ fn parse_create_view_algorithm_param() { fn parse_create_view_definer_param() { let sql = "CREATE DEFINER = 'jeffrey'@'localhost' VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3137,7 +3910,7 @@ fn parse_create_view_definer_param() { security, }), .. - } = stmt + }) = stmt { assert!(algorithm.is_none()); if let Some(GranteeName::UserHost { user, host }) = definer { @@ -3158,7 +3931,7 @@ fn parse_create_view_definer_param() { fn parse_create_view_security_param() { let sql = "CREATE SQL SECURITY DEFINER VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3166,7 +3939,7 @@ fn parse_create_view_security_param() { security, }), .. - } = stmt + }) = stmt { assert!(algorithm.is_none()); assert!(definer.is_none()); @@ -3181,7 +3954,7 @@ fn parse_create_view_security_param() { fn parse_create_view_multiple_params() { let sql = "CREATE ALGORITHM = UNDEFINED DEFINER = `root`@`%` SQL SECURITY INVOKER VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3189,7 +3962,7 @@ fn parse_create_view_multiple_params() { security, }), .. - } = stmt + }) = stmt { assert_eq!(algorithm, Some(CreateViewAlgorithm::Undefined)); if let Some(GranteeName::UserHost { user, host }) = definer { @@ -3228,6 +4001,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)"); @@ -3237,3 +4015,350 @@ fn parse_double_precision() { "CREATE TABLE foo (bar DOUBLE(11,0))", ); } + +#[test] +fn parse_looks_like_single_line_comment() { + mysql().one_statement_parses_to( + "UPDATE account SET balance=balance--1 WHERE account_id=5752", + "UPDATE account SET balance = balance - -1 WHERE account_id = 5752", + ); + mysql().one_statement_parses_to( + r#" + UPDATE account SET balance=balance-- 1 + WHERE account_id=5752 + "#, + "UPDATE account SET balance = balance WHERE account_id = 5752", + ); +} + +#[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().verified_stmt(sql_create_trigger); + assert_eq!( + create_stmt, + Statement::CreateTrigger(CreateTrigger { + or_alter: false, + temporary: false, + or_replace: false, + is_constraint: false, + name: ObjectName::from(vec![Ident::new("emp_stamp")]), + period: Some(TriggerPeriod::Before), + period_before_table: true, + events: vec![TriggerEvent::Insert], + table_name: ObjectName::from(vec![Ident::new("emp")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), + condition: None, + exec_body: Some(TriggerExecBody { + exec_type: TriggerExecBodyType::Function, + func_desc: FunctionDesc { + name: ObjectName::from(vec![Ident::new("emp_stamp")]), + 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;"; + let drop_stmt = mysql().one_statement_parses_to(sql_drop_trigger, ""); + assert_eq!( + drop_stmt, + Statement::DropTrigger(DropTrigger { + if_exists: false, + trigger_name: ObjectName::from(vec![Ident::new("emp_stamp")]), + table_name: None, + option: None, + }) + ); +} + +#[test] +fn parse_cast_integers() { + mysql().verified_expr("CAST(foo AS UNSIGNED)"); + mysql().verified_expr("CAST(foo AS SIGNED)"); + mysql().verified_expr("CAST(foo AS UNSIGNED INTEGER)"); + mysql().verified_expr("CAST(foo AS SIGNED INTEGER)"); + + mysql() + .run_parser_method("CAST(foo AS UNSIGNED(3))", |p| p.parse_expr()) + .expect_err("CAST doesn't allow display width"); + mysql() + .run_parser_method("CAST(foo AS UNSIGNED(3) INTEGER)", |p| p.parse_expr()) + .expect_err("CAST doesn't allow display width"); + mysql() + .run_parser_method("CAST(foo AS UNSIGNED INTEGER(3))", |p| p.parse_expr()) + .expect_err("CAST doesn't allow display width"); +} + +#[test] +fn parse_match_against_with_alias() { + let sql = "SELECT tbl.ProjectID FROM surveys.tbl1 AS tbl WHERE MATCH (tbl.ReferenceID) AGAINST ('AAA' IN BOOLEAN MODE)"; + match mysql().verified_stmt(sql) { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match select.selection { + Some(Expr::MatchAgainst { + columns, + match_value, + opt_search_modifier, + }) => { + assert_eq!( + columns, + vec![ObjectName::from(vec![ + Ident::new("tbl"), + Ident::new("ReferenceID") + ])] + ); + assert_eq!(match_value, Value::SingleQuotedString("AAA".to_owned())); + assert_eq!(opt_search_modifier, Some(SearchModifier::InBooleanMode)); + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + } +} + +#[test] +fn test_variable_assignment_using_colon_equal() { + let sql_select = "SELECT @price := price, @tax := price * 0.1 FROM products WHERE id = 1"; + let stmt = mysql().verified_stmt(sql_select); + match stmt { + Statement::Query(query) => { + let select = query.body.as_select().unwrap(); + + assert_eq!( + select.projection, + vec![ + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "@price".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Assignment, + right: Box::new(Expr::Identifier(Ident { + value: "price".to_string(), + quote_style: None, + span: Span::empty(), + })), + }), + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "@tax".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Assignment, + right: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "price".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Multiply, + right: Box::new(Expr::Value( + (test_utils::number("0.1")).with_empty_span() + )), + }), + }), + ] + ); + + assert_eq!( + select.selection, + Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "id".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value((test_utils::number("1")).with_empty_span())), + }) + ); + } + _ => panic!("Unexpected statement {stmt}"), + } + + let sql_update = + "UPDATE products SET price = @new_price := price * 1.1 WHERE category = 'Books'"; + let stmt = mysql().verified_stmt(sql_update); + + match stmt { + Statement::Update(Update { assignments, .. }) => { + assert_eq!( + assignments, + vec![Assignment { + target: AssignmentTarget::ColumnName(ObjectName(vec![ + ObjectNamePart::Identifier(Ident { + value: "price".to_string(), + quote_style: None, + span: Span::empty(), + }) + ])), + value: Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "@new_price".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Assignment, + right: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "price".to_string(), + quote_style: None, + span: Span::empty(), + })), + op: BinaryOperator::Multiply, + right: Box::new(Expr::Value( + (test_utils::number("1.1")).with_empty_span() + )), + }), + }, + }] + ) + } + _ => panic!("Unexpected statement {stmt}"), + } +} + +#[test] +fn parse_straight_join() { + mysql().verified_stmt( + "SELECT a.*, b.* FROM table_a AS a STRAIGHT_JOIN table_b AS b ON a.b_id = b.id", + ); + // Without table alias + 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 864fb5eb3..87cb43edd 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, @@ -363,7 +364,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "customer_id".into(), data_type: DataType::Integer(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Default( @@ -374,7 +374,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "store_id".into(), data_type: DataType::SmallInt(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -388,7 +387,6 @@ fn parse_create_table_with_defaults() { unit: None } )), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -402,11 +400,18 @@ fn parse_create_table_with_defaults() { unit: None } )), - collation: Some(ObjectName(vec![Ident::with_quote('"', "es_ES")])), - options: vec![ColumnOptionDef { - name: None, - option: ColumnOption::NotNull, - }], + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Collation(ObjectName::from(vec![ + Ident::with_quote('"', "es_ES") + ])), + }, + ColumnOptionDef { + name: None, + option: ColumnOption::NotNull, + } + ], }, ColumnDef { name: "email".into(), @@ -416,13 +421,11 @@ fn parse_create_table_with_defaults() { unit: None } )), - collation: None, options: vec![], }, ColumnDef { name: "address_id".into(), data_type: DataType::SmallInt(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull @@ -431,11 +434,12 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "activebool".into(), data_type: DataType::Boolean, - collation: None, options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Default(Expr::Value(Value::Boolean(true))), + option: ColumnOption::Default(Expr::Value( + (Value::Boolean(true)).with_empty_span() + )), }, ColumnOptionDef { name: None, @@ -446,7 +450,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "create_date".into(), data_type: DataType::Date, - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -461,7 +464,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "last_update".into(), data_type: DataType::Timestamp(None, TimezoneInfo::WithoutTimeZone), - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -476,7 +478,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "active".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull @@ -485,20 +486,25 @@ 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![ SqlOption::KeyValue { key: "fillfactor".into(), - value: Expr::Value(number("20")) + value: Expr::value(number("20")) }, SqlOption::KeyValue { key: "user_catalog_table".into(), - value: Expr::Value(Value::Boolean(true)) + value: Expr::Value((Value::Boolean(true)).with_empty_span()) }, SqlOption::KeyValue { key: "autovacuum_vacuum_threshold".into(), - value: Expr::Value(number("100")) + value: Expr::value(number("100")) }, ] ); @@ -599,10 +605,12 @@ fn parse_alter_table_constraints_unique_nulls_distinct() { match pg_and_generic() .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, .. - }) => { + Statement::AlterTable(alter_table) => match &alter_table.operations[0] { + AlterTableOperation::AddConstraint { + constraint: TableConstraint::Unique(constraint), + .. + } => { + let nulls_distinct = &constraint.nulls_distinct; assert_eq!(nulls_distinct, &NullsDistinctOption::NotDistinct) } _ => unreachable!(), @@ -666,102 +674,99 @@ fn parse_create_extension() { fn parse_drop_extension() { assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: false, cascade_or_restrict: None, - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name, extension_name2 CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into(), "extension_name2".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name, extension_name2 RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into(), "extension_name2".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: true, cascade_or_restrict: None, - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); assert_eq!( pg_and_generic() .verified_stmt("DROP EXTENSION IF EXISTS extension_name1, extension_name2 CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name1".into(), "extension_name2".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic() .verified_stmt("DROP EXTENSION IF EXISTS extension_name1, extension_name2 RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name1".into(), "extension_name2".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); } #[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( @@ -770,12 +775,14 @@ fn parse_alter_table_alter_column() { ) { AlterTableOperation::AlterColumn { column_name, op } => { assert_eq!("is_active", column_name.to_string()); - let using_expr = Expr::Value(Value::SingleQuotedString("text".to_string())); + let using_expr = + Expr::Value(Value::SingleQuotedString("text".to_string()).with_empty_span()); assert_eq!( op, AlterColumnOperation::SetDataType { data_type: DataType::Text, using: Some(using_expr), + had_set: true, } ); } @@ -822,14 +829,13 @@ fn parse_alter_table_alter_column_add_generated() { #[test] fn parse_alter_table_add_columns() { match pg().verified_stmt("ALTER TABLE IF EXISTS ONLY tab ADD COLUMN a TEXT, ADD COLUMN b INT") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, - location: _, - on_cluster: _, - } => { + .. + }) => { assert_eq!(name.to_string(), "tab"); assert!(if_exists); assert!(only); @@ -842,7 +848,6 @@ fn parse_alter_table_add_columns() { column_def: ColumnDef { name: "a".into(), data_type: DataType::Text, - collation: None, options: vec![], }, column_position: None, @@ -853,7 +858,6 @@ fn parse_alter_table_add_columns() { column_def: ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![], }, column_position: None, @@ -905,14 +909,13 @@ fn parse_alter_table_owner_to() { for case in test_cases { match pg_and_generic().verified_stmt(case.sql) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists: _, only: _, operations, - location: _, - on_cluster: _, - } => { + .. + }) => { assert_eq!(name.to_string(), "tab"); assert_eq!( operations, @@ -989,6 +992,7 @@ fn parse_create_schema_if_not_exists() { Statement::CreateSchema { if_not_exists: true, schema_name, + .. } => assert_eq!("schema_name", schema_name.to_string()), _ => unreachable!(), } @@ -1040,7 +1044,7 @@ fn test_copy_from() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1058,7 +1062,7 @@ fn test_copy_from() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1076,7 +1080,7 @@ fn test_copy_from() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1100,7 +1104,7 @@ fn test_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1118,7 +1122,7 @@ fn test_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1136,7 +1140,7 @@ fn test_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1177,7 +1181,7 @@ fn parse_copy_from() { pg_and_generic().one_statement_parses_to(sql, ""), Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["table".into()]), + table_name: ObjectName::from(vec!["table".into()]), columns: vec!["a".into(), "b".into()], }, to: false, @@ -1223,7 +1227,7 @@ fn parse_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1241,7 +1245,7 @@ fn parse_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["country".into()]), + table_name: ObjectName::from(vec!["country".into()]), columns: vec![], }, to: true, @@ -1258,7 +1262,7 @@ fn parse_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["country".into()]), + table_name: ObjectName::from(vec!["country".into()]), columns: vec![], }, to: true, @@ -1284,7 +1288,7 @@ fn parse_copy_to() { top_before_distinct: false, projection: vec![ SelectItem::ExprWithAlias { - expr: Expr::Value(number("42")), + expr: Expr::value(number("42")), alias: Ident { value: "a".into(), quote_style: None, @@ -1292,7 +1296,9 @@ fn parse_copy_to() { }, }, SelectItem::ExprWithAlias { - expr: Expr::Value(Value::SingleQuotedString("hello".into())), + expr: Expr::Value( + (Value::SingleQuotedString("hello".into())).with_empty_span() + ), alias: Ident { value: "b".into(), quote_style: None, @@ -1300,6 +1306,7 @@ fn parse_copy_to() { }, } ], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1315,16 +1322,16 @@ fn parse_copy_to() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), to: true, target: CopyTarget::File { @@ -1344,7 +1351,7 @@ fn parse_copy_from_before_v9_0() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1373,7 +1380,7 @@ fn parse_copy_from_before_v9_0() { pg_and_generic().one_statement_parses_to(sql, ""), Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1401,7 +1408,7 @@ fn parse_copy_to_before_v9_0() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1430,79 +1437,77 @@ fn parse_set() { let stmt = pg_and_generic().verified_stmt("SET a = b"); assert_eq!( stmt, - Statement::SetVariable { - local: false, + Statement::Set(Set::SingleAssignment { + scope: None, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), - value: vec![Expr::Identifier(Ident { + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::Identifier(Ident { value: "b".into(), quote_style: None, span: Span::empty(), })], - } + }) ); let stmt = pg_and_generic().verified_stmt("SET a = 'b'"); assert_eq!( stmt, - Statement::SetVariable { - local: false, + Statement::Set(Set::SingleAssignment { + scope: None, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), - value: vec![Expr::Value(Value::SingleQuotedString("b".into()))], - } + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::Value( + (Value::SingleQuotedString("b".into())).with_empty_span() + )], + }) ); let stmt = pg_and_generic().verified_stmt("SET a = 0"); assert_eq!( stmt, - Statement::SetVariable { - local: false, + Statement::Set(Set::SingleAssignment { + scope: None, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), - value: vec![Expr::Value(number("0"))], - } + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::value(number("0"))], + }) ); let stmt = pg_and_generic().verified_stmt("SET a = DEFAULT"); assert_eq!( stmt, - Statement::SetVariable { - local: false, + Statement::Set(Set::SingleAssignment { + scope: None, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), - value: vec![Expr::Identifier(Ident::new("DEFAULT"))], - } + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::Identifier(Ident::new("DEFAULT"))], + }) ); let stmt = pg_and_generic().verified_stmt("SET LOCAL a = b"); assert_eq!( stmt, - Statement::SetVariable { - local: true, + Statement::Set(Set::SingleAssignment { + scope: Some(ContextModifier::Local), hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), - value: vec![Expr::Identifier("b".into())], - } + variable: ObjectName::from(vec![Ident::new("a")]), + values: vec![Expr::Identifier("b".into())], + }) ); let stmt = pg_and_generic().verified_stmt("SET a.b.c = b"); assert_eq!( stmt, - Statement::SetVariable { - local: false, + Statement::Set(Set::SingleAssignment { + scope: None, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![ - Ident::new("a"), - Ident::new("b"), - Ident::new("c") - ])), - value: vec![Expr::Identifier(Ident { + variable: ObjectName::from(vec![Ident::new("a"), Ident::new("b"), Ident::new("c")]), + values: vec![Expr::Identifier(Ident { value: "b".into(), quote_style: None, span: Span::empty(), })], - } + }) ); let stmt = pg_and_generic().one_statement_parses_to( @@ -1511,22 +1516,21 @@ fn parse_set() { ); assert_eq!( stmt, - Statement::SetVariable { - local: false, + Statement::Set(Set::SingleAssignment { + scope: None, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![ + variable: ObjectName::from(vec![ Ident::new("hive"), Ident::new("tez"), Ident::new("auto"), Ident::new("reducer"), Ident::new("parallelism") - ])), - value: vec![Expr::Value(Value::Boolean(false))], - } + ]), + values: vec![Expr::Value((Value::Boolean(false)).with_empty_span())], + }) ); pg_and_generic().one_statement_parses_to("SET a TO b", "SET a = b"); - pg_and_generic().one_statement_parses_to("SET SESSION a = b", "SET a = b"); assert_eq!( pg_and_generic().parse_sql_statements("SET"), @@ -1556,10 +1560,10 @@ fn parse_set_role() { let stmt = pg_and_generic().verified_stmt(query); assert_eq!( stmt, - Statement::SetRole { - context_modifier: ContextModifier::Session, + Statement::Set(Set::SetRole { + context_modifier: Some(ContextModifier::Session), role_name: None, - } + }) ); assert_eq!(query, stmt.to_string()); @@ -1567,14 +1571,14 @@ fn parse_set_role() { let stmt = pg_and_generic().verified_stmt(query); assert_eq!( stmt, - Statement::SetRole { - context_modifier: ContextModifier::Local, + Statement::Set(Set::SetRole { + context_modifier: Some(ContextModifier::Local), role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\"'), span: Span::empty(), }), - } + }) ); assert_eq!(query, stmt.to_string()); @@ -1582,14 +1586,14 @@ fn parse_set_role() { let stmt = pg_and_generic().verified_stmt(query); assert_eq!( stmt, - Statement::SetRole { - context_modifier: ContextModifier::None, + Statement::Set(Set::SetRole { + context_modifier: None, role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\''), span: Span::empty(), }), - } + }) ); assert_eq!(query, stmt.to_string()); } @@ -1658,10 +1662,14 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: ObjectName(vec!["a".into()]), + name: Some(ObjectName::from(vec!["a".into()])), parameters: vec![], has_parentheses: false, - using: vec![] + using: vec![], + immediate: false, + into: vec![], + output: false, + default: false, } ); @@ -1669,13 +1677,17 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: ObjectName(vec!["a".into()]), + name: Some(ObjectName::from(vec!["a".into()])), parameters: vec![ - Expr::Value(number("1")), - Expr::Value(Value::SingleQuotedString("t".to_string())) + Expr::value(number("1")), + Expr::Value((Value::SingleQuotedString("t".to_string())).with_empty_span()) ], has_parentheses: true, - using: vec![] + using: vec![], + immediate: false, + into: vec![], + output: false, + default: false, } ); @@ -1684,23 +1696,37 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: ObjectName(vec!["a".into()]), + name: Some(ObjectName::from(vec!["a".into()])), parameters: vec![], has_parentheses: false, using: vec![ - Expr::Cast { - kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::Number("1337".parse().unwrap(), false))), - data_type: DataType::SmallInt(None), - format: None + ExprWithAlias { + expr: Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::Value( + (Value::Number("1337".parse().unwrap(), false)).with_empty_span() + )), + data_type: DataType::SmallInt(None), + format: None + }, + alias: None }, - Expr::Cast { - kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::Number("7331".parse().unwrap(), false))), - data_type: DataType::SmallInt(None), - format: None + ExprWithAlias { + expr: Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::Value( + (Value::Number("7331".parse().unwrap(), false)).with_empty_span() + )), + data_type: DataType::SmallInt(None), + format: None + }, + alias: None }, - ] + ], + immediate: false, + into: vec![], + output: false, + default: false, } ); } @@ -1793,7 +1819,9 @@ fn parse_pg_on_conflict() { assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from( + vec!["dname".into()] + )), value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "dname".into()]) },], selection: None @@ -1824,14 +1852,18 @@ fn parse_pg_on_conflict() { OnConflictAction::DoUpdate(DoUpdate { assignments: vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ + "dname".into() + ])), value: Expr::CompoundIdentifier(vec![ "EXCLUDED".into(), "dname".into() ]) }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["area".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ + "area".into() + ])), value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "area".into()]) }, ], @@ -1881,8 +1913,12 @@ fn parse_pg_on_conflict() { assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), - value: Expr::Value(Value::Placeholder("$1".to_string())) + target: AssignmentTarget::ColumnName(ObjectName::from( + vec!["dname".into()] + )), + value: Expr::Value( + (Value::Placeholder("$1".to_string())).with_empty_span() + ) },], selection: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident { @@ -1891,7 +1927,9 @@ fn parse_pg_on_conflict() { span: Span::empty(), })), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(Value::Placeholder("$2".to_string()))) + right: Box::new(Expr::Value( + (Value::Placeholder("$2".to_string())).with_empty_span() + )) }) }), action @@ -1915,12 +1953,19 @@ fn parse_pg_on_conflict() { })), .. }) => { - assert_eq!(vec![Ident::from("distributors_did_pkey")], cname.0); + assert_eq!( + ObjectName::from(vec![Ident::from("distributors_did_pkey")]), + cname + ); assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), - value: Expr::Value(Value::Placeholder("$1".to_string())) + target: AssignmentTarget::ColumnName(ObjectName::from( + vec!["dname".into()] + )), + value: Expr::Value( + (Value::Placeholder("$1".to_string())).with_empty_span() + ) },], selection: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident { @@ -1929,7 +1974,9 @@ fn parse_pg_on_conflict() { span: Span::empty(), })), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(Value::Placeholder("$2".to_string()))) + right: Box::new(Expr::Value( + (Value::Placeholder("$2".to_string())).with_empty_span() + )) }) }), action @@ -1962,7 +2009,7 @@ fn parse_pg_returning() { RETURNING temp_lo AS lo, temp_hi AS hi, prcp", ); match stmt { - Statement::Update { returning, .. } => { + Statement::Update(Update { returning, .. }) => { assert_eq!( Some(vec![ SelectItem::ExprWithAlias { @@ -2044,12 +2091,8 @@ fn parse_pg_custom_binary_ops() { let operators = [ // PostGIS "&&&", // n-D bounding boxes intersect - "&<", // (is strictly to the left of) - "&>", // (is strictly to the right of) "|=|", // distance between A and B trajectories at their closest point of approach "<<#>>", // n-D distance between A and B bounding boxes - "|>>", // A's bounding box is strictly above B's. - "~=", // bounding box is the same // PGroonga "&@", // Full text search by a keyword "&@~", // Full text search by easy to use query language @@ -2101,13 +2144,11 @@ fn parse_ampersand_arobase() { #[test] fn parse_pg_unary_ops() { let pg_unary_ops = &[ - ("~", UnaryOperator::PGBitwiseNot), ("|/", UnaryOperator::PGSquareRoot), ("||/", UnaryOperator::PGCubeRoot), ("!!", UnaryOperator::PGPrefixFactorial), ("@", UnaryOperator::PGAbs), ]; - for (str_op, op) in pg_unary_ops { let select = pg().verified_only_select(&format!("SELECT {}a", &str_op)); assert_eq!( @@ -2145,17 +2186,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()))), + left: Box::new(Expr::Value(single_quoted_string("abc").with_empty_span(),)), op: op.clone(), - right: Box::new(Expr::Value(Value::SingleQuotedString("^a".into()))), + 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] @@ -2167,23 +2230,41 @@ 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()))), + 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()))), + 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] fn parse_array_index_expr() { let num: Vec = (0..=10) - .map(|s| Expr::Value(number(&s.to_string()))) + .map(|s| Expr::Value(number(&s.to_string()).with_empty_span())) .collect(); let sql = "SELECT foo[0] FROM foos"; @@ -2294,7 +2375,7 @@ fn parse_array_subscript() { ( "(ARRAY[1, 2, 3, 4, 5, 6])[2]", Subscript::Index { - index: Expr::Value(number("2")), + index: Expr::value(number("2")), }, ), ( @@ -2306,17 +2387,17 @@ fn parse_array_subscript() { ( "(ARRAY[1, 2, 3, 4, 5, 6])[2:5]", Subscript::Slice { - lower_bound: Some(Expr::Value(number("2"))), - upper_bound: Some(Expr::Value(number("5"))), + lower_bound: Some(Expr::value(number("2"))), + upper_bound: Some(Expr::value(number("5"))), stride: None, }, ), ( "(ARRAY[1, 2, 3, 4, 5, 6])[2:5:3]", Subscript::Slice { - lower_bound: Some(Expr::Value(number("2"))), - upper_bound: Some(Expr::Value(number("5"))), - stride: Some(Expr::Value(number("3"))), + lower_bound: Some(Expr::value(number("2"))), + upper_bound: Some(Expr::value(number("5"))), + stride: Some(Expr::value(number("3"))), }, ), ( @@ -2325,12 +2406,12 @@ fn parse_array_subscript() { lower_bound: Some(Expr::BinaryOp { left: Box::new(call("array_length", [Expr::Identifier(Ident::new("arr"))])), op: BinaryOperator::Minus, - right: Box::new(Expr::Value(number("3"))), + right: Box::new(Expr::value(number("3"))), }), upper_bound: Some(Expr::BinaryOp { left: Box::new(call("array_length", [Expr::Identifier(Ident::new("arr"))])), op: BinaryOperator::Minus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), stride: None, }, @@ -2339,14 +2420,14 @@ fn parse_array_subscript() { "(ARRAY[1, 2, 3, 4, 5, 6])[:5]", Subscript::Slice { lower_bound: None, - upper_bound: Some(Expr::Value(number("5"))), + upper_bound: Some(Expr::value(number("5"))), stride: None, }, ), ( "(ARRAY[1, 2, 3, 4, 5, 6])[2:]", Subscript::Slice { - lower_bound: Some(Expr::Value(number("2"))), + lower_bound: Some(Expr::value(number("2"))), upper_bound: None, stride: None, }, @@ -2382,19 +2463,19 @@ fn parse_array_multi_subscript() { root: Box::new(call( "make_array", vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")) + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")) ] )), access_chain: vec![ AccessExpr::Subscript(Subscript::Slice { - lower_bound: Some(Expr::Value(number("1"))), - upper_bound: Some(Expr::Value(number("2"))), + lower_bound: Some(Expr::value(number("1"))), + upper_bound: Some(Expr::value(number("2"))), stride: None, }), AccessExpr::Subscript(Subscript::Index { - index: Expr::Value(number("2")), + index: Expr::value(number("2")), }), ], }, @@ -2404,7 +2485,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)), @@ -2418,6 +2499,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); @@ -2428,6 +2511,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!(), } @@ -2435,7 +2520,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, @@ -2449,6 +2534,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); @@ -2459,14 +2546,297 @@ 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!(), + } +} + +#[test] +/// Test to verify the correctness of parsing the `CREATE INDEX` statement with optional operator classes. +/// +/// # Implementative details +/// +/// At this time, since the parser library is not intended to take care of the semantics of the SQL statements, +/// there is no way to verify the correctness of the operator classes, nor whether they are valid for the given +/// index type. This test is only intended to verify that the parser can correctly parse the statement. For this +/// reason, the test includes a `totally_not_valid` operator class. +fn parse_create_indices_with_operator_classes() { + let indices = [ + IndexType::GIN, + IndexType::GiST, + IndexType::SPGiST, + IndexType::Custom("CustomIndexType".into()), + ]; + let operator_classes: [Option; 4] = [ + None, + Some("gin_trgm_ops".into()), + Some("gist_trgm_ops".into()), + Some("totally_not_valid".into()), + ]; + + for expected_index_type in indices { + 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}")) + .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}")) + .unwrap_or_default() + ); + + let expected_function_column = IndexColumn { + column: OrderByExpr { + expr: Expr::Function(Function { + name: ObjectName(vec![ObjectNamePart::Identifier(Ident { + value: "concat_users_name".to_owned(), + quote_style: None, + span: Span::empty(), + })]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier( + Ident { + value: "first_name".to_owned(), + quote_style: None, + span: Span::empty(), + }, + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier( + Ident { + value: "last_name".to_owned(), + quote_style: None, + span: Span::empty(), + }, + ))), + ], + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: expected_operator_class.clone(), + }; + + match pg().verified_stmt(&single_column_sql_statement) { + Statement::CreateIndex(CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using: Some(using), + columns, + unique: false, + concurrently: false, + if_not_exists: false, + include, + nulls_distinct: None, + with, + predicate: None, + index_options, + alter_options, + }) => { + assert_eq_vec(&["the_index_name"], &name); + assert_eq_vec(&["users"], &table_name); + assert_eq!(expected_index_type, using); + 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!(), + } + + match pg().verified_stmt(&multi_column_sql_statement) { + Statement::CreateIndex(CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using: Some(using), + columns, + unique: false, + concurrently: false, + if_not_exists: false, + include, + nulls_distinct: None, + with, + predicate: None, + index_options, + alter_options, + }) => { + assert_eq_vec(&["the_index_name"], &name); + assert_eq_vec(&["users"], &table_name); + assert_eq!(expected_index_type, using); + assert_eq!( + IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(Ident { + value: "column_name".to_owned(), + quote_style: None, + span: Span::empty() + }), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: None + }, + columns[0], + ); + 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!(), + } + } + } +} + +#[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)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex(CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using: Some(using), + columns, + unique: false, + concurrently: false, + if_not_exists: false, + include, + nulls_distinct: None, + with, + predicate: None, + index_options, + alter_options, + }) => { + assert_eq_vec(&["bloomidx"], &name); + assert_eq_vec(&["tbloom"], &table_name); + assert_eq!(IndexType::Bloom, using); + assert_eq_vec(&["i1", "i2", "i3"], &columns); + assert!(include.is_empty()); + assert_eq!( + vec![ + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("length"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("80").into())), + }, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col1"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("2").into())), + }, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col2"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("2").into())), + }, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col3"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("4").into())), + }, + ], + with + ); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_brin() { + let sql = "CREATE INDEX brin_sensor_data_recorded_at ON sensor_data USING BRIN (recorded_at)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex(CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using: Some(using), + columns, + unique: false, + concurrently: false, + if_not_exists: false, + include, + 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); + assert_eq!(IndexType::BRIN, using); + 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!(), + } +} + +#[test] +fn parse_create_table_with_inherits() { + let single_inheritance_sql = + "CREATE TABLE child_table (child_column INT) INHERITS (public.parent_table)"; + match pg().verified_stmt(single_inheritance_sql) { + Statement::CreateTable(CreateTable { + inherits: Some(inherits), + .. + }) => { + assert_eq_vec(&["public", "parent_table"], &inherits[0].0); + } + _ => unreachable!(), + } + + let double_inheritance_sql = "CREATE TABLE child_table (child_column INT) INHERITS (public.parent_table, pg_catalog.pg_settings)"; + match pg().verified_stmt(double_inheritance_sql) { + Statement::CreateTable(CreateTable { + inherits: Some(inherits), + .. + }) => { + assert_eq_vec(&["public", "parent_table"], &inherits[0].0); + assert_eq_vec(&["pg_catalog", "pg_settings"], &inherits[1].0); } _ => unreachable!(), } } +#[test] +fn parse_create_table_with_empty_inherits_fails() { + assert!(matches!( + pg().parse_sql_statements("CREATE TABLE child_table (child_column INT) INHERITS ()"), + Err(ParserError::ParserError(_)) + )); +} + #[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)), @@ -2480,6 +2850,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); @@ -2490,6 +2862,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!(), } @@ -2497,7 +2871,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)), @@ -2511,6 +2885,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); @@ -2521,6 +2897,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!(), } @@ -2528,7 +2906,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)), @@ -2542,6 +2920,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); @@ -2550,8 +2930,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!(), } @@ -2559,7 +2941,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)), @@ -2573,6 +2955,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); @@ -2584,11 +2968,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)), @@ -2602,6 +2988,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); @@ -2613,6 +3001,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!(), } @@ -2624,7 +3014,7 @@ fn parse_array_subquery_expr() { let select = pg().verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("ARRAY")]), + name: ObjectName::from(vec![Ident::new("ARRAY")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::Subquery(Box::new(Query { @@ -2637,7 +3027,10 @@ fn parse_array_subquery_expr() { distinct: None, top: None, top_before_distinct: false, - projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))], + projection: vec![SelectItem::UnnamedExpr(Expr::Value( + (number("1")).with_empty_span() + ))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2653,13 +3046,17 @@ fn parse_array_subquery_expr() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), right: 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("2")))], + projection: vec![SelectItem::UnnamedExpr(Expr::Value( + (number("2")).with_empty_span() + ))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2675,17 +3072,17 @@ fn parse_array_subquery_expr() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), }), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), filter: None, null_treatment: None, @@ -2701,16 +3098,16 @@ fn test_transaction_statement() { let statement = pg().verified_stmt("SET TRANSACTION SNAPSHOT '000003A1-1'"); assert_eq!( statement, - Statement::SetTransaction { + Statement::Set(Set::SetTransaction { modes: vec![], snapshot: Some(Value::SingleQuotedString(String::from("000003A1-1"))), session: false - } + }) ); let statement = pg().verified_stmt("SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE"); assert_eq!( statement, - Statement::SetTransaction { + Statement::Set(Set::SetTransaction { modes: vec![ TransactionMode::AccessMode(TransactionAccessMode::ReadOnly), TransactionMode::AccessMode(TransactionAccessMode::ReadWrite), @@ -2718,7 +3115,7 @@ fn test_transaction_statement() { ], snapshot: None, session: true - } + }) ); } @@ -2730,7 +3127,9 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("params"))), op: BinaryOperator::LongArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString("name".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("name".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2741,7 +3140,9 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("params"))), op: BinaryOperator::Arrow, - right: Box::new(Expr::Value(Value::SingleQuotedString("name".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("name".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2753,12 +3154,14 @@ fn test_json() { left: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::Arrow, - right: Box::new(Expr::Value(Value::SingleQuotedString("items".to_string()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("items".to_string())).with_empty_span() + )) }), op: BinaryOperator::LongArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "product".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("product".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2770,7 +3173,7 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("obj"))), op: BinaryOperator::Arrow, - right: Box::new(Expr::Value(number("42"))), + right: Box::new(Expr::value(number("42"))), }), select.projection[0] ); @@ -2795,9 +3198,9 @@ fn test_json() { left: Box::new(Expr::Identifier(Ident::new("obj"))), op: BinaryOperator::Arrow, right: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("3"))), + left: Box::new(Expr::value(number("3"))), op: BinaryOperator::Multiply, - right: Box::new(Expr::Value(number("2"))), + right: Box::new(Expr::value(number("2"))), }), }), select.projection[0] @@ -2809,9 +3212,9 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::HashArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "{a,b,c}".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("{a,b,c}".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2822,9 +3225,9 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::HashLongArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "{a,b,c}".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("{a,b,c}".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2835,9 +3238,9 @@ fn test_json() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::AtArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "{\"a\": 1}".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("{\"a\": 1}".to_string())).with_empty_span() + )), }, select.selection.unwrap(), ); @@ -2846,9 +3249,9 @@ fn test_json() { let select = pg().verified_only_select(sql); assert_eq!( Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString( - "{\"a\": 1}".to_string() - ))), + left: Box::new(Expr::Value( + (Value::SingleQuotedString("{\"a\": 1}".to_string())).with_empty_span() + )), op: BinaryOperator::ArrowAt, right: Box::new(Expr::Identifier(Ident::new("info"))), }, @@ -2863,8 +3266,8 @@ fn test_json() { op: BinaryOperator::HashMinus, right: Box::new(Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("a".to_string())), - Expr::Value(Value::SingleQuotedString("b".to_string())), + Expr::Value((Value::SingleQuotedString("a".to_string())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("b".to_string())).with_empty_span()), ], named: true, })), @@ -2878,7 +3281,9 @@ fn test_json() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::from("info"))), op: BinaryOperator::AtQuestion, - right: Box::new(Expr::Value(Value::SingleQuotedString("$.a".to_string())),), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("$.a".to_string())).with_empty_span() + ),), }, select.selection.unwrap(), ); @@ -2889,7 +3294,9 @@ fn test_json() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::from("info"))), op: BinaryOperator::AtAt, - right: Box::new(Expr::Value(Value::SingleQuotedString("$.a".to_string())),), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("$.a".to_string())).with_empty_span() + ),), }, select.selection.unwrap(), ); @@ -2900,7 +3307,9 @@ fn test_json() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::Question, - right: Box::new(Expr::Value(Value::SingleQuotedString("b".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("b".to_string())).with_empty_span() + )), }, select.selection.unwrap(), ); @@ -2913,8 +3322,8 @@ fn test_json() { op: BinaryOperator::QuestionAnd, right: Box::new(Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("b".to_string())), - Expr::Value(Value::SingleQuotedString("c".to_string())) + Expr::Value((Value::SingleQuotedString("b".to_string())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("c".to_string())).with_empty_span()) ], named: true })) @@ -2930,8 +3339,8 @@ fn test_json() { op: BinaryOperator::QuestionPipe, right: Box::new(Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("b".to_string())), - Expr::Value(Value::SingleQuotedString("c".to_string())) + Expr::Value((Value::SingleQuotedString("b".to_string())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("c".to_string())).with_empty_span()) ], named: true })) @@ -2941,18 +3350,99 @@ fn test_json() { } #[test] -fn test_fn_arg_with_value_operator() { +fn json_object_colon_syntax() { + match pg().verified_expr("JSON_OBJECT('name' : 'value')") { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => { + assert!( + matches!( + &args[..], + &[FunctionArg::ExprNamed { + operator: FunctionArgOperator::Colon, + .. + }] + ), + "Invalid function argument: {args:?}" + ); + } + other => panic!( + "Expected: JSON_OBJECT('name' : 'value') to be parsed as a function, but got {other:?}" + ), + } +} + +#[test] +fn json_object_value_syntax() { match pg().verified_expr("JSON_OBJECT('name' VALUE 'value')") { Expr::Function(Function { args: FunctionArguments::List(FunctionArgumentList { args, .. }), .. }) => { 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:?}"), } } +#[test] +fn parse_json_object() { + let sql = "JSON_OBJECT('name' VALUE 'value' NULL ON NULL)"; + let expr = pg().verified_expr(sql); + assert!( + matches!( + expr.clone(), + Expr::Function(Function { + name: ObjectName(parts), + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) if parts == vec![ObjectNamePart::Identifier(Ident::new("JSON_OBJECT"))] + && matches!( + &args[..], + &[FunctionArg::ExprNamed { operator: FunctionArgOperator::Value, .. }] + ) + && clauses == vec![FunctionArgumentClause::JsonNullClause(JsonNullClause::NullOnNull)] + ), + "Failed to parse JSON_OBJECT with expected structure, got: {expr:?}" + ); + + let sql = "JSON_OBJECT('name' VALUE 'value' RETURNING JSONB)"; + let expr = pg().verified_expr(sql); + assert!( + matches!( + expr.clone(), + Expr::Function(Function { + name: ObjectName(parts), + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) if parts == vec![ObjectNamePart::Identifier(Ident::new("JSON_OBJECT"))] + && matches!( + &args[..], + &[FunctionArg::ExprNamed { operator: FunctionArgOperator::Value, .. }] + ) + && clauses == vec![FunctionArgumentClause::JsonReturningClause(JsonReturningClause { data_type: DataType::JSONB })] + ), + "Failed to parse JSON_OBJECT with expected structure, got: {expr:?}" + ); + + let sql = "JSON_OBJECT(RETURNING JSONB)"; + let expr = pg().verified_expr(sql); + assert!( + matches!( + expr.clone(), + Expr::Function(Function { + name: ObjectName(parts), + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) if parts == vec![ObjectNamePart::Identifier(Ident::new("JSON_OBJECT"))] + && args.is_empty() + && clauses == vec![FunctionArgumentClause::JsonReturningClause(JsonReturningClause { data_type: DataType::JSONB })] + ), + "Failed to parse JSON_OBJECT with expected structure, got: {expr:?}" + ); +} + #[test] fn parse_json_table_is_not_reserved() { // JSON_TABLE is not a reserved keyword in PostgreSQL, even though it is in SQL:2023 @@ -2963,7 +3453,10 @@ fn parse_json_table_is_not_reserved() { TableFactor::Table { name: ObjectName(name), .. - } => assert_eq!("JSON_TABLE", name[0].value), + } => assert_eq!( + ObjectNamePart::Identifier(Ident::new("JSON_TABLE")), + name[0] + ), other => panic!("Expected: JSON_TABLE to be parsed as a table name, but got {other:?}"), } } @@ -2972,39 +3465,46 @@ fn parse_json_table_is_not_reserved() { fn test_composite_value() { let sql = "SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9"; let select = pg().verified_only_select(sql); + + let Expr::CompoundFieldAccess { root, access_chain } = + expr_from_projection(&select.projection[0]) + else { + unreachable!("expected projection: got {:?}", &select.projection[0]); + }; assert_eq!( - SelectItem::UnnamedExpr(Expr::CompositeAccess { - key: Ident::new("name"), - expr: Box::new(Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ - Ident::new("on_hand"), - Ident::new("item") - ])))) - }), - select.projection[0] + root.as_ref(), + &Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("on_hand"), + Ident::new("item") + ]))) + ); + assert_eq!( + access_chain.as_slice(), + &[AccessExpr::Dot(Expr::Identifier(Ident::new("name")))] ); assert_eq!( - select.selection, - Some(Expr::BinaryOp { - left: Box::new(Expr::CompositeAccess { - key: Ident::new("price"), - expr: Box::new(Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ + select.selection.as_ref().unwrap(), + &Expr::BinaryOp { + left: Box::new(Expr::CompoundFieldAccess { + root: Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ Ident::new("on_hand"), Ident::new("item") - ])))) + ]))) + .into(), + access_chain: vec![AccessExpr::Dot(Expr::Identifier(Ident::new("price")))] }), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(number("9"))) - }) + right: Box::new(Expr::value(number("9"))) + } ); let sql = "SELECT (information_schema._pg_expandarray(ARRAY['i', 'i'])).n"; let select = pg().verified_only_select(sql); assert_eq!( - SelectItem::UnnamedExpr(Expr::CompositeAccess { - key: Ident::new("n"), - expr: Box::new(Expr::Nested(Box::new(Expr::Function(Function { - name: ObjectName(vec![ + &Expr::CompoundFieldAccess { + root: Box::new(Expr::Nested(Box::new(Expr::Function(Function { + name: ObjectName::from(vec![ Ident::new("information_schema"), Ident::new("_pg_expandarray") ]), @@ -3015,8 +3515,12 @@ fn test_composite_value() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Array( Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("i".to_string())), - Expr::Value(Value::SingleQuotedString("i".to_string())), + Expr::Value( + (Value::SingleQuotedString("i".to_string())).with_empty_span() + ), + Expr::Value( + (Value::SingleQuotedString("i".to_string())).with_empty_span() + ), ], named: true } @@ -3027,9 +3531,10 @@ fn test_composite_value() { filter: None, over: None, within_group: vec![], - })))) - }), - select.projection[0] + })))), + access_chain: vec![AccessExpr::Dot(Expr::Identifier(Ident::new("n")))], + }, + expr_from_projection(&select.projection[0]) ); } @@ -3075,27 +3580,27 @@ fn parse_escaped_literal_string() { let select = pg_and_generic().verified_only_select(sql); assert_eq!(6, select.projection.len()); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("s1 \n s1".to_string())), + &Expr::Value((Value::EscapedStringLiteral("s1 \n s1".to_string())).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("s2 \\n s2".to_string())), + &Expr::Value((Value::EscapedStringLiteral("s2 \\n s2".to_string())).with_empty_span()), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("s3 \\\n s3".to_string())), + &Expr::Value((Value::EscapedStringLiteral("s3 \\\n s3".to_string())).with_empty_span()), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("s4 \\\\n s4".to_string())), + &Expr::Value((Value::EscapedStringLiteral("s4 \\\\n s4".to_string())).with_empty_span()), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("'".to_string())), + &Expr::Value((Value::EscapedStringLiteral("'".to_string())).with_empty_span()), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("foo \\".to_string())), + &Expr::Value((Value::EscapedStringLiteral("foo \\".to_string())).with_empty_span()), expr_from_projection(&select.projection[5]) ); @@ -3113,31 +3618,31 @@ fn parse_escaped_literal_string() { let select = pg_and_generic().verified_only_select_with_canonical(sql, canonical); assert_eq!(7, select.projection.len()); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("\u{0001}".to_string())), + &Expr::Value((Value::EscapedStringLiteral("\u{0001}".to_string())).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("\u{10ffff}".to_string())), + &Expr::Value((Value::EscapedStringLiteral("\u{10ffff}".to_string())).with_empty_span()), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("\u{000c}".to_string())), + &Expr::Value((Value::EscapedStringLiteral("\u{000c}".to_string())).with_empty_span()), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("%".to_string())), + &Expr::Value((Value::EscapedStringLiteral("%".to_string())).with_empty_span()), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("\u{0002}".to_string())), + &Expr::Value((Value::EscapedStringLiteral("\u{0002}".to_string())).with_empty_span()), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("%".to_string())), + &Expr::Value((Value::EscapedStringLiteral("%".to_string())).with_empty_span()), expr_from_projection(&select.projection[5]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("%".to_string())), + &Expr::Value((Value::EscapedStringLiteral("%".to_string())).with_empty_span()), expr_from_projection(&select.projection[6]) ); @@ -3185,7 +3690,7 @@ fn parse_current_functions() { let select = pg_and_generic().verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("CURRENT_CATALOG")]), + name: ObjectName::from(vec![Ident::new("CURRENT_CATALOG")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -3198,7 +3703,7 @@ fn parse_current_functions() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("CURRENT_USER")]), + name: ObjectName::from(vec![Ident::new("CURRENT_USER")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -3211,7 +3716,7 @@ fn parse_current_functions() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("SESSION_USER")]), + name: ObjectName::from(vec![Ident::new("SESSION_USER")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -3224,7 +3729,7 @@ fn parse_current_functions() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("USER")]), + name: ObjectName::from(vec![Ident::new("USER")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -3279,7 +3784,9 @@ fn parse_custom_operator() { "pg_catalog".into(), "~".into() ]), - right: Box::new(Expr::Value(Value::SingleQuotedString("^(table)$".into()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("^(table)$".into())).with_empty_span() + )) }) ); @@ -3295,7 +3802,9 @@ fn parse_custom_operator() { span: Span::empty(), })), op: BinaryOperator::PGCustomBinaryOperator(vec!["pg_catalog".into(), "~".into()]), - right: Box::new(Expr::Value(Value::SingleQuotedString("^(table)$".into()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("^(table)$".into())).with_empty_span() + )) }) ); @@ -3311,7 +3820,9 @@ fn parse_custom_operator() { span: Span::empty(), })), op: BinaryOperator::PGCustomBinaryOperator(vec!["~".into()]), - right: Box::new(Expr::Value(Value::SingleQuotedString("^(table)$".into()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("^(table)$".into())).with_empty_span() + )) }) ); } @@ -3320,47 +3831,29 @@ fn parse_custom_operator() { fn parse_create_role() { let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; match pg().verified_stmt(sql) { - Statement::CreateRole { - names, - if_not_exists, - .. - } => { - assert_eq_vec(&["mysql_a", "mysql_b"], &names); - assert!(if_not_exists); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["mysql_a", "mysql_b"], &create_role.names); + assert!(create_role.if_not_exists); } _ => unreachable!(), } let sql = "CREATE ROLE abc LOGIN PASSWORD NULL"; match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, - login, - password, - .. - }], - ) => { - assert_eq_vec(&["abc"], names); - assert_eq!(*login, Some(true)); - assert_eq!(*password, Some(Password::NullPassword)); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["abc"], &create_role.names); + assert_eq!(create_role.login, Some(true)); + assert_eq!(create_role.password, Some(Password::NullPassword)); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let sql = "CREATE ROLE abc WITH LOGIN PASSWORD NULL"; match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, - login, - password, - .. - }], - ) => { - assert_eq_vec(&["abc"], names); - assert_eq!(*login, Some(true)); - assert_eq!(*password, Some(Password::NullPassword)); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["abc"], &create_role.names); + assert_eq!(create_role.login, Some(true)); + assert_eq!(create_role.password, Some(Password::NullPassword)); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } @@ -3368,67 +3861,44 @@ fn parse_create_role() { let sql = "CREATE ROLE magician WITH SUPERUSER CREATEROLE NOCREATEDB BYPASSRLS INHERIT PASSWORD 'abcdef' LOGIN VALID UNTIL '2025-01-01' IN ROLE role1, role2 ROLE role3 ADMIN role4, role5 REPLICATION"; // Roundtrip order of optional parameters is not preserved match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, - if_not_exists, - bypassrls, - login, - inherit, - password, - superuser, - create_db, - create_role, - replication, - connection_limit, - valid_until, - in_role, - in_group, - role, - user: _, - admin, - authorization_owner, - }], - ) => { - assert_eq_vec(&["magician"], names); - assert!(!*if_not_exists); - assert_eq!(*login, Some(true)); - assert_eq!(*inherit, Some(true)); - assert_eq!(*bypassrls, Some(true)); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["magician"], &create_role.names); + assert!(!create_role.if_not_exists); + assert_eq!(create_role.login, Some(true)); + assert_eq!(create_role.inherit, Some(true)); + assert_eq!(create_role.bypassrls, Some(true)); assert_eq!( - *password, - Some(Password::Password(Expr::Value(Value::SingleQuotedString( - "abcdef".into() - )))) + create_role.password, + Some(Password::Password(Expr::Value( + (Value::SingleQuotedString("abcdef".into())).with_empty_span() + ))) ); - assert_eq!(*superuser, Some(true)); - assert_eq!(*create_db, Some(false)); - assert_eq!(*create_role, Some(true)); - assert_eq!(*replication, Some(true)); - assert_eq!(*connection_limit, None); + assert_eq!(create_role.superuser, Some(true)); + assert_eq!(create_role.create_db, Some(false)); + assert_eq!(create_role.create_role, Some(true)); + assert_eq!(create_role.replication, Some(true)); + assert_eq!(create_role.connection_limit, None); assert_eq!( - *valid_until, - Some(Expr::Value(Value::SingleQuotedString("2025-01-01".into()))) + create_role.valid_until, + Some(Expr::Value( + (Value::SingleQuotedString("2025-01-01".into())).with_empty_span() + )) ); - assert_eq_vec(&["role1", "role2"], in_role); - assert!(in_group.is_empty()); - assert_eq_vec(&["role3"], role); - assert_eq_vec(&["role4", "role5"], admin); - assert_eq!(*authorization_owner, None); + assert_eq_vec(&["role1", "role2"], &create_role.in_role); + assert!(create_role.in_group.is_empty()); + assert_eq_vec(&["role3"], &create_role.role); + assert_eq_vec(&["role4", "role5"], &create_role.admin); + assert_eq!(create_role.authorization_owner, None); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let sql = "CREATE ROLE abc WITH USER foo, bar ROLE baz "; match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, user, role, .. - }], - ) => { - assert_eq_vec(&["abc"], names); - assert_eq_vec(&["foo", "bar"], user); - assert_eq_vec(&["baz"], role); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["abc"], &create_role.names); + assert_eq_vec(&["foo", "bar"], &create_role.user); + assert_eq_vec(&["baz"], &create_role.role); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } @@ -3490,13 +3960,15 @@ fn parse_alter_role() { RoleOption::Login(true), RoleOption::Replication(true), RoleOption::BypassRLS(true), - RoleOption::ConnectionLimit(Expr::Value(number("100"))), + RoleOption::ConnectionLimit(Expr::value(number("100"))), RoleOption::Password({ - Password::Password(Expr::Value(Value::SingleQuotedString("abcdef".into()))) + Password::Password(Expr::Value( + (Value::SingleQuotedString("abcdef".into())).with_empty_span(), + )) }), - RoleOption::ValidUntil(Expr::Value(Value::SingleQuotedString( - "2025-01-01".into(), - ))) + RoleOption::ValidUntil(Expr::Value( + (Value::SingleQuotedString("2025-01-01".into(),)).with_empty_span() + )) ] }, } @@ -3536,7 +4008,7 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Set { - config_name: ObjectName(vec![Ident { + config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), @@ -3557,13 +4029,15 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Set { - config_name: ObjectName(vec![Ident { + config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), }]), - config_value: SetConfigValue::Value(Expr::Value(number("100000"))), - in_database: Some(ObjectName(vec![Ident { + config_value: SetConfigValue::Value(Expr::Value( + (number("100000")).with_empty_span() + )), + in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, span: Span::empty(), @@ -3582,13 +4056,15 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Set { - config_name: ObjectName(vec![Ident { + config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), }]), - config_value: SetConfigValue::Value(Expr::Value(number("100000"))), - in_database: Some(ObjectName(vec![Ident { + config_value: SetConfigValue::Value(Expr::Value( + (number("100000")).with_empty_span() + )), + in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, span: Span::empty(), @@ -3607,13 +4083,13 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Set { - config_name: ObjectName(vec![Ident { + config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), }]), config_value: SetConfigValue::Default, - in_database: Some(ObjectName(vec![Ident { + in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, span: Span::empty(), @@ -3648,12 +4124,12 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Reset { - config_name: ResetConfig::ConfigName(ObjectName(vec![Ident { + config_name: ResetConfig::ConfigName(ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), }])), - in_database: Some(ObjectName(vec![Ident { + in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, span: Span::empty(), @@ -3679,7 +4155,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -3698,7 +4177,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -3746,15 +4225,226 @@ 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;'"; assert_eq!( pg_and_generic().verified_stmt(sql), Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace: false, temporary: false, - name: ObjectName(vec![Ident::new("add")]), + name: ObjectName::from(vec![Ident::new("add")]), args: Some(vec![ OperateFunctionArg::unnamed(DataType::Integer(None)), OperateFunctionArg::unnamed(DataType::Integer(None)), @@ -3765,7 +4455,7 @@ fn parse_create_function() { called_on_null: Some(FunctionCalledOnNull::Strict), parallel: Some(FunctionParallel::Safe), function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - Value::SingleQuotedString("select $1 + $2;".into()) + (Value::SingleQuotedString("select $1 + $2;".into())).with_empty_span() ))), if_not_exists: false, using: None, @@ -3783,6 +4473,8 @@ fn parse_create_function_detailed() { pg_and_generic().verified_stmt("CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL STABLE PARALLEL UNSAFE RETURN a + b"); pg_and_generic().verified_stmt("CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL STABLE CALLED ON NULL INPUT PARALLEL UNSAFE RETURN a + b"); pg_and_generic().verified_stmt(r#"CREATE OR REPLACE FUNCTION increment(i INTEGER) RETURNS INTEGER LANGUAGE plpgsql AS $$ BEGIN RETURN i + 1; END; $$"#); + pg_and_generic().verified_stmt(r#"CREATE OR REPLACE FUNCTION no_arg() RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN DELETE FROM my_table; END; $$"#); + pg_and_generic().verified_stmt(r#"CREATE OR REPLACE FUNCTION return_table(i INTEGER) RETURNS TABLE(id UUID, is_active BOOLEAN) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT NULL::UUID, NULL::BOOLEAN; END; $$"#); } #[test] fn parse_incorrect_create_function_parallel() { @@ -3795,10 +4487,10 @@ fn parse_drop_function() { let sql = "DROP FUNCTION IF EXISTS test_func"; assert_eq!( pg().verified_stmt(sql), - Statement::DropFunction { + Statement::DropFunction(DropFunction { if_exists: true, func_desc: vec![FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_func".to_string(), quote_style: None, span: Span::empty(), @@ -3806,16 +4498,16 @@ fn parse_drop_function() { args: None }], drop_behavior: None - } + }) ); let sql = "DROP FUNCTION IF EXISTS test_func(a INTEGER, IN b INTEGER = 1)"; assert_eq!( pg().verified_stmt(sql), - Statement::DropFunction { + Statement::DropFunction(DropFunction { if_exists: true, func_desc: vec![FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_func".to_string(), quote_style: None, span: Span::empty(), @@ -3826,22 +4518,24 @@ fn parse_drop_function() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), }], drop_behavior: None - } + }) ); let sql = "DROP FUNCTION IF EXISTS test_func1(a INTEGER, IN b INTEGER = 1), test_func2(a VARCHAR, IN b INTEGER = 1)"; assert_eq!( pg().verified_stmt(sql), - Statement::DropFunction { + Statement::DropFunction(DropFunction { if_exists: true, func_desc: vec![ FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_func1".to_string(), quote_style: None, span: Span::empty(), @@ -3852,15 +4546,14 @@ fn parse_drop_function() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - ))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), }, FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_func2".to_string(), quote_style: None, span: Span::empty(), @@ -3871,16 +4564,75 @@ fn parse_drop_function() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - ))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), } ], drop_behavior: None - } + }) + ); +} + +#[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) + }) ); } @@ -3892,7 +4644,7 @@ fn parse_drop_procedure() { Statement::DropProcedure { if_exists: true, proc_desc: vec![FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_proc".to_string(), quote_style: None, span: Span::empty(), @@ -3909,7 +4661,7 @@ fn parse_drop_procedure() { Statement::DropProcedure { if_exists: true, proc_desc: vec![FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_proc".to_string(), quote_style: None, span: Span::empty(), @@ -3920,7 +4672,9 @@ fn parse_drop_procedure() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), }], @@ -3935,7 +4689,7 @@ fn parse_drop_procedure() { if_exists: true, proc_desc: vec![ FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_proc1".to_string(), quote_style: None, span: Span::empty(), @@ -3946,15 +4700,14 @@ fn parse_drop_procedure() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - ))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), }, FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_proc2".to_string(), quote_style: None, span: Span::empty(), @@ -3965,10 +4718,9 @@ fn parse_drop_procedure() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - ))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), } @@ -4005,58 +4757,76 @@ fn parse_dollar_quoted_string() { }; assert_eq!( - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: None, - value: "hello".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: None, + value: "hello".into() + })) + .with_empty_span() + ), expr_from_projection(&projection[0]) ); assert_eq!( - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: Some("tag_name".into()), - value: "world".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: Some("tag_name".into()), + value: "world".into() + })) + .with_empty_span() + ), expr_from_projection(&projection[1]) ); assert_eq!( - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: None, - value: "Foo$Bar".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: None, + value: "Foo$Bar".into() + })) + .with_empty_span() + ), expr_from_projection(&projection[2]) ); assert_eq!( projection[3], SelectItem::ExprWithAlias { - expr: Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: None, - value: "Foo$Bar".into(), - })), + expr: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: None, + value: "Foo$Bar".into(), + })) + .with_empty_span() + ), alias: Ident { value: "col_name".into(), quote_style: None, span: Span::empty(), }, - } + }, ); assert_eq!( expr_from_projection(&projection[4]), - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: None, - value: "".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: None, + value: "".into() + })) + .with_empty_span() + ), ); assert_eq!( expr_from_projection(&projection[5]), - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: Some("tag_name".into()), - value: "".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: Some("tag_name".into()), + value: "".into() + })) + .with_empty_span() + ), ); } @@ -4136,20 +4906,20 @@ fn parse_select_group_by_cube() { #[test] fn parse_truncate() { let truncate = pg_and_generic().verified_stmt("TRUNCATE db.table_name"); - let table_name = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]); + 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 { + Statement::Truncate(Truncate { table_names, partitions: None, table: false, - only: false, identity: None, cascade: None, on_cluster: None, - }, + }), truncate ); } @@ -4159,21 +4929,21 @@ fn parse_truncate_with_options() { let truncate = pg_and_generic() .verified_stmt("TRUNCATE TABLE ONLY db.table_name RESTART IDENTITY CASCADE"); - let table_name = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]); + 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!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: true, - only: true, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, - }, + }), truncate ); } @@ -4184,28 +4954,29 @@ fn parse_truncate_with_table_list() { "TRUNCATE TABLE db.table_name, db.other_table_name RESTART IDENTITY CASCADE", ); - let table_name_a = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]); - let table_name_b = ObjectName(vec![Ident::new("db"), Ident::new("other_table_name")]); + let table_name_a = ObjectName::from(vec![Ident::new("db"), Ident::new("table_name")]); + let table_name_b = ObjectName::from(vec![Ident::new("db"), Ident::new("other_table_name")]); let table_names = vec![ TruncateTableTarget { name: table_name_a.clone(), + only: false, }, TruncateTableTarget { name: table_name_b.clone(), + only: false, }, ]; assert_eq!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: true, - only: false, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, - }, + }), truncate ); } @@ -4233,7 +5004,6 @@ fn parse_create_table_with_alias() { name, columns, constraints, - with_options: _with_options, if_not_exists: false, external: false, file_format: None, @@ -4247,37 +5017,31 @@ fn parse_create_table_with_alias() { ColumnDef { name: "int8_col".into(), data_type: DataType::Int8(None), - collation: None, options: vec![] }, ColumnDef { name: "int4_col".into(), data_type: DataType::Int4(None), - collation: None, options: vec![] }, ColumnDef { name: "int2_col".into(), data_type: DataType::Int2(None), - collation: None, options: vec![] }, ColumnDef { name: "float8_col".into(), data_type: DataType::Float8, - collation: None, options: vec![] }, ColumnDef { name: "float4_col".into(), data_type: DataType::Float4, - collation: None, options: vec![] }, ColumnDef { name: "bool_col".into(), data_type: DataType::Bool, - collation: None, options: vec![] }, ] @@ -4299,13 +5063,11 @@ fn parse_create_table_with_partition_by() { ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![] }, ColumnDef { name: "b".into(), data_type: DataType::Text, - collation: None, options: vec![] } ], @@ -4352,7 +5114,7 @@ fn parse_join_constraint_unnest_alias() { with_ordinality: false, }, global: false, - join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), op: BinaryOperator::Eq, right: Box::new(Expr::Identifier("c2".into())), @@ -4381,7 +5143,7 @@ fn test_simple_postgres_insert_with_alias() { or: None, ignore: false, into: true, - table: TableObject::TableName(ObjectName(vec![Ident { + table: TableObject::TableName(ObjectName::from(vec![Ident { value: "test_tables".to_string(), quote_style: None, span: Span::empty(), @@ -4407,21 +5169,21 @@ fn test_simple_postgres_insert_with_alias() { source: Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Identifier(Ident::new("DEFAULT")), - Expr::Value(Value::Number("123".to_string(), false)) + Expr::Value((Value::Number("123".to_string(), false)).with_empty_span()) ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), assignments: vec![], partitioned: None, @@ -4451,7 +5213,7 @@ fn test_simple_postgres_insert_with_alias() { or: None, ignore: false, into: true, - table: TableObject::TableName(ObjectName(vec![Ident { + table: TableObject::TableName(ObjectName::from(vec![Ident { value: "test_tables".to_string(), quote_style: None, span: Span::empty(), @@ -4477,24 +5239,24 @@ fn test_simple_postgres_insert_with_alias() { source: Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Identifier(Ident::new("DEFAULT")), - Expr::Value(Value::Number( - bigdecimal::BigDecimal::new(123.into(), 0), - false - )) + Expr::Value( + (Value::Number(bigdecimal::BigDecimal::new(123.into(), 0), false)) + .with_empty_span() + ) ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), assignments: vec![], partitioned: None, @@ -4523,7 +5285,7 @@ fn test_simple_insert_with_quoted_alias() { or: None, ignore: false, into: true, - table: TableObject::TableName(ObjectName(vec![Ident { + table: TableObject::TableName(ObjectName::from(vec![Ident { value: "test_tables".to_string(), quote_style: None, span: Span::empty(), @@ -4549,21 +5311,23 @@ fn test_simple_insert_with_quoted_alias() { source: Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Identifier(Ident::new("DEFAULT")), - Expr::Value(Value::SingleQuotedString("0123".to_string())) + Expr::Value( + (Value::SingleQuotedString("0123".to_string())).with_empty_span() + ) ]] })), order_by: None, - limit: None, - limit_by: vec![], - offset: None, + limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, + pipe_operators: vec![], })), assignments: vec![], partitioned: None, @@ -4616,24 +5380,28 @@ 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: "2001-09-28 01:00".to_owned(), - }), + 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(Value::SingleQuotedString( - "America/Los_Angeles".to_owned(), - ))), + expr: Box::new(Expr::Value( + Value::SingleQuotedString("America/Los_Angeles".to_owned()).with_empty_span(), + )), data_type: DataType::Text, format: None, }), }), op: BinaryOperator::Plus, right: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "23 hours".to_owned(), - ))), + value: Box::new(Expr::Value( + Value::SingleQuotedString("23 hours".to_owned()).with_empty_span(), + )), leading_field: None, leading_precision: None, last_field: None, @@ -4648,20 +5416,64 @@ 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 { key: "foo".into(), - value: Expr::Value(Value::SingleQuotedString("bar".into())), + value: Expr::Value( + (Value::SingleQuotedString("bar".into())).with_empty_span() + ), }, SqlOption::KeyValue { key: "a".into(), - value: Expr::Value(number("123")), + value: Expr::value(number("123")), }, ], with_options @@ -4707,37 +5519,147 @@ fn test_table_unnest_with_ordinality() { #[test] fn test_escaped_string_literal() { match pg().verified_expr(r#"E'\n'"#) { - Expr::Value(Value::EscapedStringLiteral(s)) => { + Expr::Value(ValueWithSpan { + value: Value::EscapedStringLiteral(s), + span: _, + }) => { assert_eq!("\n", s); } _ => unreachable!(), } } +#[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![CheckConstraint { + 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, + } + .into()], + }); + + 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![CheckConstraint { + 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, + } + .into()], + }); + + 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![CheckConstraint { + 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, + } + .into()], + }); + + 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![CheckConstraint { + 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, + } + .into()], + }); + + 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![CheckConstraint { + 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, + } + .into()], + }); + + 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 { + let expected = Statement::CreateTrigger(CreateTrigger { + or_alter: false, + temporary: false, or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("check_insert")]), - period: TriggerPeriod::Before, + name: ObjectName::from(vec![Ident::new("check_insert")]), + period: Some(TriggerPeriod::Before), + period_before_table: true, events: vec![TriggerEvent::Insert], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_insert")]), + name: ObjectName::from(vec![Ident::new("check_account_insert")]), args: None, }, - }, + }), + statements_as: false, + statements: None, characteristics: None, - }; + }); assert_eq!(pg().verified_stmt(sql), expected); } @@ -4745,34 +5667,38 @@ fn parse_create_simple_before_insert_trigger() { #[test] 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 { + let expected = Statement::CreateTrigger(CreateTrigger { + or_alter: false, + temporary: false, or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("check_update")]), - period: TriggerPeriod::After, + name: ObjectName::from(vec![Ident::new("check_update")]), + period: Some(TriggerPeriod::After), + period_before_table: true, events: vec![TriggerEvent::Update(vec![])], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: Some(Expr::Nested(Box::new(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("NEW"), Ident::new("balance"), ])), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(number("10000"))), + right: Box::new(Expr::value(number("10000"))), }))), - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_update")]), + name: ObjectName::from(vec![Ident::new("check_account_update")]), args: None, }, - }, + }), + statements_as: false, + statements: None, characteristics: None, - }; + }); assert_eq!(pg().verified_stmt(sql), expected); } @@ -4780,27 +5706,31 @@ fn parse_create_after_update_trigger_with_condition() { #[test] 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 { + let expected = Statement::CreateTrigger(CreateTrigger { + or_alter: false, + temporary: false, or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("check_delete")]), - period: TriggerPeriod::InsteadOf, + name: ObjectName::from(vec![Ident::new("check_delete")]), + period: Some(TriggerPeriod::InsteadOf), + period_before_table: true, events: vec![TriggerEvent::Delete], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_deletes")]), + name: ObjectName::from(vec![Ident::new("check_account_deletes")]), args: None, }, - }, + }), + statements_as: false, + statements: None, characteristics: None, - }; + }); assert_eq!(pg().verified_stmt(sql), expected); } @@ -4808,35 +5738,39 @@ fn parse_create_instead_of_delete_trigger() { #[test] 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 { + let expected = Statement::CreateTrigger(CreateTrigger { + or_alter: false, + temporary: false, or_replace: false, is_constraint: true, - name: ObjectName(vec![Ident::new("check_multiple_events")]), - period: TriggerPeriod::Before, + name: ObjectName::from(vec![Ident::new("check_multiple_events")]), + period: Some(TriggerPeriod::Before), + period_before_table: true, events: vec![ TriggerEvent::Insert, TriggerEvent::Update(vec![]), TriggerEvent::Delete, ], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_changes")]), + 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), enforced: None, }), - }; + }); assert_eq!(pg().verified_stmt(sql), expected); } @@ -4844,38 +5778,42 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { #[test] 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 { + let expected = Statement::CreateTrigger(CreateTrigger { + or_alter: false, + temporary: false, or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("check_referencing")]), - period: TriggerPeriod::Before, + name: ObjectName::from(vec![Ident::new("check_referencing")]), + period: Some(TriggerPeriod::Before), + period_before_table: true, events: vec![TriggerEvent::Insert], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![ TriggerReferencing { refer_type: TriggerReferencingType::NewTable, is_as: true, - transition_relation_name: ObjectName(vec![Ident::new("new_accounts")]), + transition_relation_name: ObjectName::from(vec![Ident::new("new_accounts")]), }, TriggerReferencing { refer_type: TriggerReferencingType::OldTable, is_as: true, - transition_relation_name: ObjectName(vec![Ident::new("old_accounts")]), + transition_relation_name: ObjectName::from(vec![Ident::new("old_accounts")]), }, ], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_referencing")]), + name: ObjectName::from(vec![Ident::new("check_account_referencing")]), args: None, }, - }, + }), + statements_as: false, + statements: None, characteristics: None, - }; + }); assert_eq!(pg().verified_stmt(sql), expected); } @@ -4889,11 +5827,11 @@ fn parse_create_trigger_invalid_cases() { let invalid_cases = vec![ ( "CREATE TRIGGER check_update BEFORE UPDATE ON accounts FUNCTION check_account_update", - "Expected: FOR, found: FUNCTION" + "Expected: an SQL statement, found: FUNCTION" ), ( "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 INSERT or UPDATE or DELETE or TRUNCATE, found: TOMORROW" ), ( "CREATE TRIGGER check_update BEFORE SAVE ON accounts EXECUTE FUNCTION check_account_update", @@ -4922,17 +5860,17 @@ 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!( pg().verified_stmt(sql), - Statement::DropTrigger { + Statement::DropTrigger(DropTrigger { if_exists, - trigger_name: ObjectName(vec![Ident::new("check_update")]), - table_name: ObjectName(vec![Ident::new("table_name")]), + trigger_name: ObjectName::from(vec![Ident::new("check_update")]), + table_name: Some(ObjectName::from(vec![Ident::new("table_name")])), option - } + }) ); } } @@ -5016,8 +5954,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(); @@ -5040,33 +5977,31 @@ fn parse_trigger_related_functions() { temporary: false, external: false, global: None, + dynamic: false, if_not_exists: false, transient: false, volatile: false, - name: ObjectName(vec![Ident::new("emp")]), + iceberg: false, + name: ObjectName::from(vec![Ident::new("emp")]), columns: vec![ ColumnDef { name: "empname".into(), data_type: DataType::Text, - collation: None, options: vec![], }, ColumnDef { name: "salary".into(), data_type: DataType::Integer(None), - collation: None, options: vec![], }, ColumnDef { name: "last_date".into(), data_type: DataType::Timestamp(None, TimezoneInfo::None), - collation: None, options: vec![], }, ColumnDef { name: "last_user".into(), data_type: DataType::Text, - collation: None, options: vec![], }, ], @@ -5078,19 +6013,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, @@ -5098,7 +6027,7 @@ fn parse_trigger_related_functions() { partition_by: None, cluster_by: None, clustered_by: None, - options: None, + inherits: None, strict: false, copy_grants: false, enable_schema_evolution: None, @@ -5109,6 +6038,18 @@ fn parse_trigger_related_functions() { with_aggregation_policy: None, with_row_access_policy: None, with_tags: None, + base_location: None, + external_volume: None, + 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, } ); @@ -5117,15 +6058,16 @@ fn parse_trigger_related_functions() { assert_eq!( create_function, Statement::CreateFunction(CreateFunction { + or_alter: false, or_replace: false, temporary: false, if_not_exists: false, - name: ObjectName(vec![Ident::new("emp_stamp")]), - args: None, + name: ObjectName::from(vec![Ident::new("emp_stamp")]), + args: Some(vec![]), return_type: Some(DataType::Trigger), function_body: Some( CreateFunctionBody::AsBeforeOptions( - Expr::Value( + Expr::Value(( Value::DollarQuotedString( DollarQuotedString { value: "\n BEGIN\n -- Check that empname and salary are given\n IF NEW.empname IS NULL THEN\n RAISE EXCEPTION 'empname cannot be null';\n END IF;\n IF NEW.salary IS NULL THEN\n RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n END IF;\n\n -- Who works for us when they must pay for it?\n IF NEW.salary < 0 THEN\n RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n END IF;\n\n -- Remember who changed the payroll when\n NEW.last_date := current_timestamp;\n NEW.last_user := current_user;\n RETURN NEW;\n END;\n ".to_owned(), @@ -5133,8 +6075,8 @@ fn parse_trigger_related_functions() { "emp_stamp".to_owned(), ), }, - ), - ), + ) + ).with_empty_span()), ), ), behavior: None, @@ -5152,38 +6094,42 @@ fn parse_trigger_related_functions() { assert_eq!( create_trigger, - Statement::CreateTrigger { + Statement::CreateTrigger(CreateTrigger { + or_alter: false, + temporary: false, or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("emp_stamp")]), - period: TriggerPeriod::Before, + name: ObjectName::from(vec![Ident::new("emp_stamp")]), + period: Some(TriggerPeriod::Before), + period_before_table: true, events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![])], - table_name: ObjectName(vec![Ident::new("emp")]), + table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, - exec_body: TriggerExecBody { + exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("emp_stamp")]), - args: None, + name: ObjectName::from(vec![Ident::new("emp_stamp")]), + args: Some(vec![]), } - }, + }), + statements_as: false, + statements: None, characteristics: None - } + }) ); // Check the fourth statement assert_eq!( drop_trigger, - Statement::DropTrigger { + Statement::DropTrigger(DropTrigger { if_exists: false, - trigger_name: ObjectName(vec![Ident::new("emp_stamp")]), - table_name: ObjectName(vec![Ident::new("emp")]), + trigger_name: ObjectName::from(vec![Ident::new("emp_stamp")]), + table_name: Some(ObjectName::from(vec![Ident::new("emp")])), option: None - } + }) ); } @@ -5201,7 +6147,10 @@ fn test_unicode_string_literal() { ]; for (input, expected) in pairs { match pg_and_generic().verified_expr(input) { - Expr::Value(Value::UnicodeStringLiteral(s)) => { + Expr::Value(ValueWithSpan { + value: Value::UnicodeStringLiteral(s), + span: _, + }) => { assert_eq!(expected, s); } _ => unreachable!(), @@ -5220,10 +6169,14 @@ fn check_arrow_precedence(sql: &str, arrow_operator: BinaryOperator) { span: Span::empty(), })), op: arrow_operator, - right: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("bar".to_string())).with_empty_span() + )), }), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString("spam".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("spam".to_string())).with_empty_span() + )), } ) } @@ -5253,7 +6206,9 @@ fn arrow_cast_precedence() { op: BinaryOperator::Arrow, right: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("bar".to_string())).with_empty_span() + )), data_type: DataType::Text, format: None, }), @@ -5263,15 +6218,8 @@ fn arrow_cast_precedence() { #[test] fn parse_create_type_as_enum() { - let statement = pg().one_statement_parses_to( - r#"CREATE TYPE public.my_type AS ENUM ( - 'label1', - 'label2', - 'label3', - 'label4' - );"#, - "CREATE TYPE public.my_type AS ENUM ('label1', 'label2', 'label3', 'label4')", - ); + let sql = "CREATE TYPE public.my_type AS ENUM ('label1', 'label2', 'label3', 'label4')"; + let statement = pg_and_generic().verified_stmt(sql); match statement { Statement::CreateType { name, @@ -5286,17 +6234,422 @@ fn parse_create_type_as_enum() { labels ); } - _ => unreachable!(), + _ => unreachable!("{:?} should parse to Statement::CreateType", sql), } } +#[test] +fn parse_alter_type() { + struct TestCase { + sql: &'static str, + name: &'static str, + operation: AlterTypeOperation, + } + vec![ + TestCase { + sql: "ALTER TYPE public.my_type RENAME TO my_new_type", + name: "public.my_type", + operation: AlterTypeOperation::Rename(AlterTypeRename { + new_name: Ident::new("my_new_type"), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE IF NOT EXISTS 'label3.5' BEFORE 'label4'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: true, + value: Ident::with_quote('\'', "label3.5"), + position: Some(AlterTypeAddValuePosition::Before(Ident::with_quote( + '\'', "label4", + ))), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE 'label3.5' BEFORE 'label4'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: false, + value: Ident::with_quote('\'', "label3.5"), + position: Some(AlterTypeAddValuePosition::Before(Ident::with_quote( + '\'', "label4", + ))), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE IF NOT EXISTS 'label3.5' AFTER 'label3'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: true, + value: Ident::with_quote('\'', "label3.5"), + position: Some(AlterTypeAddValuePosition::After(Ident::with_quote( + '\'', "label3", + ))), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE 'label3.5' AFTER 'label3'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: false, + value: Ident::with_quote('\'', "label3.5"), + position: Some(AlterTypeAddValuePosition::After(Ident::with_quote( + '\'', "label3", + ))), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE IF NOT EXISTS 'label5'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: true, + value: Ident::with_quote('\'', "label5"), + position: None, + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE 'label5'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: false, + value: Ident::with_quote('\'', "label5"), + position: None, + }), + }, + ] + .into_iter() + .enumerate() + .for_each(|(index, tc)| { + let statement = pg_and_generic().verified_stmt(tc.sql); + if let Statement::AlterType(AlterType { name, operation }) = statement { + assert_eq!(tc.name, name.to_string(), "TestCase[{index}].name"); + assert_eq!(tc.operation, operation, "TestCase[{index}].operation"); + } else { + unreachable!("{:?} should parse to Statement::AlterType", tc.sql); + } + }); +} + #[test] fn parse_bitstring_literal() { let select = pg_and_generic().verified_only_select("SELECT B'111'"); assert_eq!( select.projection, vec![SelectItem::UnnamedExpr(Expr::Value( - Value::SingleQuotedByteStringLiteral("111".to_string()) + (Value::SingleQuotedByteStringLiteral("111".to_string())).with_empty_span() ))] ); } + +#[test] +fn parse_varbit_datatype() { + match pg_and_generic().verified_stmt("CREATE TABLE foo (x VARBIT, y VARBIT(42))") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ + ColumnDef { + name: "x".into(), + data_type: DataType::VarBit(None), + options: vec![], + }, + ColumnDef { + name: "y".into(), + data_type: DataType::VarBit(Some(42)), + options: vec![], + } + ] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_replica_identity() { + match pg_and_generic().verified_stmt("ALTER TABLE foo REPLICA IDENTITY FULL") { + Statement::AlterTable(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(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(AlterTable { operations, .. }) => { + assert_eq!( + operations, + vec![AlterTableOperation::AddConstraint { + constraint: ForeignKeyConstraint { + 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, + match_kind: None, + characteristics: None, + } + .into(), + 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(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); + } +} + +#[test] +fn parse_alter_schema() { + match pg_and_generic().verified_stmt("ALTER SCHEMA foo RENAME TO bar") { + Statement::AlterSchema(AlterSchema { operations, .. }) => { + assert_eq!( + operations, + vec![AlterSchemaOperation::Rename { + name: ObjectName::from(vec!["bar".into()]) + }] + ); + } + _ => unreachable!(), + } + + match pg_and_generic().verified_stmt("ALTER SCHEMA foo OWNER TO bar") { + Statement::AlterSchema(AlterSchema { operations, .. }) => { + assert_eq!( + operations, + vec![AlterSchemaOperation::OwnerTo { + owner: Owner::Ident("bar".into()) + }] + ); + } + _ => unreachable!(), + } + + match pg_and_generic().verified_stmt("ALTER SCHEMA foo OWNER TO CURRENT_ROLE") { + Statement::AlterSchema(AlterSchema { operations, .. }) => { + assert_eq!( + operations, + vec![AlterSchemaOperation::OwnerTo { + owner: Owner::CurrentRole + }] + ); + } + _ => unreachable!(), + } + + match pg_and_generic().verified_stmt("ALTER SCHEMA foo OWNER TO CURRENT_USER") { + Statement::AlterSchema(AlterSchema { operations, .. }) => { + assert_eq!( + operations, + vec![AlterSchemaOperation::OwnerTo { + owner: Owner::CurrentUser + }] + ); + } + _ => unreachable!(), + } + + match pg_and_generic().verified_stmt("ALTER SCHEMA foo OWNER TO SESSION_USER") { + Statement::AlterSchema(AlterSchema { operations, .. }) => { + assert_eq!( + operations, + vec![AlterSchemaOperation::OwnerTo { + owner: Owner::SessionUser + }] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_foreign_key_match() { + let test_cases = [ + ("MATCH FULL", ConstraintReferenceMatchKind::Full), + ("MATCH SIMPLE", ConstraintReferenceMatchKind::Simple), + ("MATCH PARTIAL", ConstraintReferenceMatchKind::Partial), + ]; + + for (match_clause, expected_kind) in test_cases { + // Test column-level foreign key + let sql = format!("CREATE TABLE t (id INT REFERENCES other_table (id) {match_clause})"); + let statement = pg_and_generic().verified_stmt(&sql); + match statement { + Statement::CreateTable(CreateTable { columns, .. }) => { + match &columns[0].options[0].option { + ColumnOption::ForeignKey(constraint) => { + assert_eq!(constraint.match_kind, Some(expected_kind)); + } + _ => panic!("Expected ColumnOption::ForeignKey"), + } + } + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } + + // Test table-level foreign key constraint + let sql = format!( + "CREATE TABLE t (id INT, FOREIGN KEY (id) REFERENCES other_table(id) {match_clause})" + ); + let statement = pg_and_generic().verified_stmt(&sql); + match statement { + Statement::CreateTable(CreateTable { constraints, .. }) => match &constraints[0] { + TableConstraint::ForeignKey(constraint) => { + assert_eq!(constraint.match_kind, Some(expected_kind)); + } + _ => panic!("Expected TableConstraint::ForeignKey"), + }, + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } + } +} + +#[test] +fn parse_foreign_key_match_with_actions() { + let sql = "CREATE TABLE orders (order_id INT REFERENCES another_table (id) MATCH FULL ON DELETE CASCADE ON UPDATE RESTRICT, customer_id INT, CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH SIMPLE ON DELETE SET NULL ON UPDATE CASCADE)"; + + pg_and_generic().verified_stmt(sql); +} diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 857d378bc..90652ff48 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -39,7 +39,7 @@ fn test_square_brackets_over_db_schema_table_name() { assert_eq!( select.from[0], TableWithJoins { - relation: table_from_name(ObjectName(vec![ + relation: table_from_name(ObjectName::from(vec![ Ident { value: "test_schema".to_string(), quote_style: Some('['), @@ -81,7 +81,7 @@ fn test_double_quotes_over_db_schema_table_name() { assert_eq!( select.from[0], TableWithJoins { - relation: table_from_name(ObjectName(vec![ + relation: table_from_name(ObjectName::from(vec![ Ident { value: "test_schema".to_string(), quote_style: Some('"'), @@ -114,7 +114,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -133,7 +136,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -205,7 +208,7 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Dot { key: "o_orderkey".to_string(), @@ -228,10 +231,12 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Bracket { - key: Expr::Value(Value::SingleQuotedString("id".to_owned())) + key: Expr::Value( + (Value::SingleQuotedString("id".to_owned())).with_empty_span() + ) } ] } @@ -252,10 +257,12 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Bracket { - key: Expr::Value(Value::SingleQuotedString("id".to_owned())) + key: Expr::Value( + (Value::SingleQuotedString("id".to_owned())).with_empty_span() + ) } ] } @@ -276,7 +283,7 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Dot { key: "id".to_string(), @@ -297,13 +304,13 @@ fn test_parse_json_path_from() { TableFactor::Table { name, json_path, .. } => { - assert_eq!(name, &ObjectName(vec![Ident::new("src")])); + assert_eq!(name, &ObjectName::from(vec![Ident::new("src")])); assert_eq!( json_path, &Some(JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Dot { key: "a".to_string(), @@ -321,20 +328,22 @@ fn test_parse_json_path_from() { TableFactor::Table { name, json_path, .. } => { - assert_eq!(name, &ObjectName(vec![Ident::new("src")])); + assert_eq!(name, &ObjectName::from(vec![Ident::new("src")])); assert_eq!( json_path, &Some(JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Dot { key: "a".to_string(), quoted: false }, JsonPathElem::Bracket { - key: Expr::Value(Value::Number("1".parse().unwrap(), false)) + key: Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + ) }, JsonPathElem::Dot { key: "b".to_string(), @@ -354,7 +363,7 @@ fn test_parse_json_path_from() { } => { assert_eq!( name, - &ObjectName(vec![Ident::new("src"), Ident::new("a"), Ident::new("b")]) + &ObjectName::from(vec![Ident::new("src"), Ident::new("a"), Ident::new("b")]) ); assert_eq!(json_path, &None); } @@ -382,3 +391,64 @@ fn test_parse_nested_quoted_identifier() { .parse_sql_statements(r#"SELECT 1 AS ["1]"#) .is_err()); } + +#[test] +fn parse_extract_single_quotes() { + let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table"; + redshift().verified_stmt(sql); +} + +#[test] +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 112aa5264..f187af1bd 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -19,9 +19,8 @@ //! Test SQL syntax specific to Snowflake. The parser based on the //! generic dialect is also tested (on the inputs it can handle). -use sqlparser::ast::helpers::stmt_data_loading::{ - DataLoadingOption, DataLoadingOptionType, StageLoadSelectItem, -}; +use sqlparser::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionKind}; +use sqlparser::ast::helpers::stmt_data_loading::{StageLoadSelectItem, StageLoadSelectItemKind}; use sqlparser::ast::*; use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect}; use sqlparser::parser::{ParserError, ParserOptions}; @@ -45,6 +44,33 @@ fn test_snowflake_create_table() { } } +#[test] +fn parse_sf_create_secure_view_and_materialized_view() { + for sql in [ + "CREATE SECURE VIEW v AS SELECT 1", + "CREATE SECURE MATERIALIZED VIEW v AS SELECT 1", + "CREATE OR REPLACE SECURE VIEW v AS SELECT 1", + "CREATE OR REPLACE SECURE MATERIALIZED VIEW v AS SELECT 1", + ] { + match snowflake().verified_stmt(sql) { + Statement::CreateView(CreateView { + secure, + materialized, + .. + }) => { + assert!(secure); + if sql.contains("MATERIALIZED") { + assert!(materialized); + } else { + assert!(!materialized); + } + } + _ => unreachable!(), + } + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + } +} + #[test] fn test_snowflake_create_or_replace_table() { let sql = "CREATE OR REPLACE TABLE my_table (a number)"; @@ -271,8 +297,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 ); @@ -292,8 +318,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 ); @@ -346,7 +372,6 @@ fn test_snowflake_create_table_column_comment() { name: None, option: ColumnOption::Comment("some comment".to_string()) }], - collation: None }], columns ) @@ -448,19 +473,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 ) @@ -472,31 +534,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"; @@ -553,7 +611,6 @@ fn test_snowflake_create_table_with_autoincrement_columns() { ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Identity(IdentityPropertyKind::Autoincrement( @@ -567,15 +624,14 @@ fn test_snowflake_create_table_with_autoincrement_columns() { ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Identity(IdentityPropertyKind::Autoincrement( IdentityProperty { parameters: Some(IdentityPropertyFormatKind::FunctionCall( IdentityParameters { - seed: Expr::Value(number("100")), - increment: Expr::Value(number("1")), + seed: Expr::value(number("100")), + increment: Expr::value(number("1")), } )), order: Some(IdentityPropertyOrder::NoOrder), @@ -586,7 +642,6 @@ fn test_snowflake_create_table_with_autoincrement_columns() { ColumnDef { name: "c".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Identity(IdentityPropertyKind::Identity( @@ -600,7 +655,6 @@ fn test_snowflake_create_table_with_autoincrement_columns() { ColumnDef { name: "d".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Identity(IdentityPropertyKind::Identity( @@ -608,8 +662,12 @@ fn test_snowflake_create_table_with_autoincrement_columns() { parameters: Some( IdentityPropertyFormatKind::StartAndIncrement( IdentityParameters { - seed: Expr::Value(number("100")), - increment: Expr::Value(number("1")), + seed: Expr::Value( + (number("100")).with_empty_span() + ), + increment: Expr::Value( + (number("1")).with_empty_span() + ), } ) ), @@ -634,8 +692,12 @@ fn test_snowflake_create_table_with_collated_column() { vec![ColumnDef { name: "a".into(), data_type: DataType::Text, - collation: Some(ObjectName(vec![Ident::with_quote('\'', "de_DE")])), - options: vec![] + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Collation(ObjectName::from(vec![Ident::with_quote( + '\'', "de_DE" + )])), + }] },] ); } @@ -674,13 +736,12 @@ fn test_snowflake_create_table_with_columns_masking_policy() { vec![ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( ColumnPolicyProperty { with, - policy_name: "p".into(), + policy_name: ObjectName::from(vec![Ident::new("p")]), using_columns, } )) @@ -709,13 +770,12 @@ fn test_snowflake_create_table_with_columns_projection_policy() { vec![ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( ColumnPolicyProperty { with, - policy_name: "p".into(), + policy_name: ObjectName::from(vec![Ident::new("p")]), using_columns: None, } )) @@ -747,14 +807,19 @@ fn test_snowflake_create_table_with_columns_tags() { vec![ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, 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() + ), ] }), }], @@ -782,7 +847,6 @@ fn test_snowflake_create_table_with_several_column_options() { ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -798,7 +862,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()]), } )), @@ -808,8 +872,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() + ), ] }), } @@ -818,14 +888,19 @@ fn test_snowflake_create_table_with_several_column_options() { ColumnDef { name: "b".into(), data_type: DataType::Text, - collation: Some(ObjectName(vec![Ident::with_quote('\'', "de_DE")])), options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Collation(ObjectName::from(vec![ + Ident::with_quote('\'', "de_DE") + ])), + }, ColumnOptionDef { name: None, option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( ColumnPolicyProperty { with: false, - policy_name: "p2".into(), + policy_name: ObjectName::from(vec![Ident::new("p2")]), using_columns: None, } )), @@ -835,8 +910,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() + ), ] }), } @@ -849,6 +930,105 @@ 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 \ + 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, + external_volume, catalog, catalog_sync, + storage_serialization_policy, change_tracking, + copy_grants, data_retention_time_in_days, + max_data_extension_time_in_days, with_aggregation_policy, + with_row_access_policy, with_tags, .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some(WrappedCollection::Parentheses(vec![ + Expr::Identifier(Ident::new("a")), + Expr::Identifier(Ident::new("b")), + ])), + cluster_by + ); + assert_eq!("relative/path", base_location.unwrap()); + assert_eq!("volume", external_volume.unwrap()); + assert_eq!("SNOWFLAKE", catalog.unwrap()); + assert_eq!("OPEN_CATALOG", catalog_sync.unwrap()); + assert_eq!(StorageSerializationPolicy::Compatible, storage_serialization_policy.unwrap()); + assert!(change_tracking.unwrap()); + assert!(copy_grants); + assert_eq!(Some(5), data_retention_time_in_days); + assert_eq!(Some(10), max_data_extension_time_in_days); + assert_eq!( + Some("WITH ROW ACCESS POLICY policy_name ON (a)".to_string()), + with_row_access_policy.map(|policy| policy.to_string()) + ); + assert_eq!( + Some("policy_name".to_string()), + with_aggregation_policy.map(|name| name.to_string()) + ); + assert_eq!(Some(vec![ + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".into()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".into()), + ]), with_tags); + + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_iceberg_table() { + match snowflake() + .verified_stmt("CREATE ICEBERG TABLE my_table (a INT) BASE_LOCATION='relative_path'") + { + Statement::CreateTable(CreateTable { + name, + base_location, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!("relative_path", base_location.unwrap()); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_iceberg_table_without_location() { + let res = snowflake().parse_sql_statements("CREATE ICEBERG TABLE my_table (a INT)"); + assert_eq!( + ParserError::ParserError("BASE_LOCATION is required for ICEBERG tables".to_string()), + res.unwrap_err() + ); +} + +#[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() @@ -867,7 +1047,7 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { test_utils::TestedDialects::new(vec![Box::new(SnowflakeDialect {}) as Box]); match dialect.verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, columns, or_replace, @@ -880,7 +1060,7 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { if_not_exists, temporary, .. - } => { + }) => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); assert_eq!(options, CreateTableOptions::None); @@ -898,6 +1078,94 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { } } +#[test] +fn parse_sf_create_table_or_view_with_dollar_quoted_comment() { + // Snowflake transforms dollar quoted comments into a common comment in DDL representation of creation + snowflake() + .one_statement_parses_to( + r#"CREATE OR REPLACE TEMPORARY VIEW foo.bar.baz ("COL_1" COMMENT $$comment 1$$) COMMENT = $$view comment$$ AS (SELECT 1)"#, + r#"CREATE OR REPLACE TEMPORARY VIEW foo.bar.baz ("COL_1" COMMENT 'comment 1') COMMENT = 'view comment' AS (SELECT 1)"# + ); + + snowflake().one_statement_parses_to( + r#"CREATE TABLE my_table (a STRING COMMENT $$comment 1$$) COMMENT = $$table comment$$"#, + r#"CREATE TABLE my_table (a STRING COMMENT 'comment 1') COMMENT = 'table 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, @@ -1034,9 +1302,9 @@ fn parse_semi_structured_data_traversal() { path: JsonPath { path: vec![JsonPathElem::Bracket { key: Expr::BinaryOp { - left: Box::new(Expr::Value(number("2"))), + left: Box::new(Expr::value(number("2"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("2"))) + right: Box::new(Expr::value(number("2"))) }, }] }, @@ -1114,7 +1382,7 @@ fn parse_semi_structured_data_traversal() { quoted: false, }, JsonPathElem::Bracket { - key: Expr::Value(number("0")), + key: Expr::value(number("0")), }, JsonPathElem::Dot { key: "bar".to_owned(), @@ -1136,7 +1404,7 @@ fn parse_semi_structured_data_traversal() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")), + key: Expr::value(number("0")), }, JsonPathElem::Dot { key: "foo".to_owned(), @@ -1181,6 +1449,32 @@ fn parse_semi_structured_data_traversal() { .to_string(), "sql parser error: Expected: variant object key name, found: 42" ); + + // casting a json access and accessing an array element + assert_eq!( + snowflake().verified_expr("a:b::ARRAY[1]"), + Expr::JsonAccess { + value: Box::new(Expr::Cast { + kind: CastKind::DoubleColon, + data_type: DataType::Array(ArrayElemTypeDef::None), + format: None, + expr: Box::new(Expr::JsonAccess { + value: Box::new(Expr::Identifier(Ident::new("a"))), + path: JsonPath { + path: vec![JsonPathElem::Dot { + key: "b".to_string(), + quoted: false + }] + } + }) + }), + path: JsonPath { + path: vec![JsonPathElem::Bracket { + key: Expr::value(number("1")) + }] + } + } + ); } #[test] @@ -1199,7 +1493,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -1218,7 +1515,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -1290,7 +1587,7 @@ fn test_select_wildcard_with_exclude() { let select = snowflake_and_generic() .verified_only_select("SELECT name.* EXCLUDE department_id FROM employee_table"); let expected = SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("name")]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])), WildcardAdditionalOptions { opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), ..Default::default() @@ -1327,7 +1624,7 @@ fn test_select_wildcard_with_rename() { "SELECT name.* RENAME (department_id AS new_dep, employee_id AS new_emp) FROM employee_table", ); let expected = SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("name")]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])), WildcardAdditionalOptions { opt_rename: Some(RenameSelectItem::Multiple(vec![ IdentWithAlias { @@ -1430,7 +1727,7 @@ fn test_alter_table_clustering() { Expr::Identifier(Ident::new("c1")), Expr::Identifier(Ident::with_quote('"', "c2")), Expr::Function(Function { - name: ObjectName(vec![Ident::new("TO_DATE")]), + name: ObjectName::from(vec![Ident::new("TO_DATE")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -1456,6 +1753,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") { @@ -1558,13 +1862,13 @@ fn parse_snowflake_declare_result_set() { ( "DECLARE res RESULTSET DEFAULT 42", "res", - Some(DeclareAssignment::Default(Expr::Value(number("42")).into())), + Some(DeclareAssignment::Default(Expr::value(number("42")).into())), ), ( "DECLARE res RESULTSET := 42", "res", Some(DeclareAssignment::DuckAssignment( - Expr::Value(number("42")).into(), + Expr::value(number("42")).into(), )), ), ("DECLARE res RESULTSET", "res", None), @@ -1614,8 +1918,8 @@ fn parse_snowflake_declare_exception() { "ex", Some(DeclareAssignment::Expr( Expr::Tuple(vec![ - Expr::Value(number("42")), - Expr::Value(Value::SingleQuotedString("ERROR".to_string())), + Expr::value(number("42")), + Expr::Value((Value::SingleQuotedString("ERROR".to_string())).with_empty_span()), ]) .into(), )), @@ -1651,13 +1955,13 @@ fn parse_snowflake_declare_variable() { "DECLARE profit TEXT DEFAULT 42", "profit", Some(DataType::Text), - Some(DeclareAssignment::Default(Expr::Value(number("42")).into())), + Some(DeclareAssignment::Default(Expr::value(number("42")).into())), ), ( "DECLARE profit DEFAULT 42", "profit", None, - Some(DeclareAssignment::Default(Expr::Value(number("42")).into())), + Some(DeclareAssignment::Default(Expr::value(number("42")).into())), ), ("DECLARE profit TEXT", "profit", Some(DataType::Text), None), ("DECLARE profit", "profit", None, None), @@ -1810,38 +2114,30 @@ fn test_create_stage_with_stage_params() { "", stage_params.endpoint.unwrap() ); - assert!(stage_params - .credentials - .options - .contains(&DataLoadingOption { - option_name: "AWS_KEY_ID".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "1a2b3c".to_string() - })); - assert!(stage_params - .credentials - .options - .contains(&DataLoadingOption { - option_name: "AWS_SECRET_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "4x5y6z".to_string() - })); - assert!(stage_params - .encryption - .options - .contains(&DataLoadingOption { - option_name: "MASTER_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "key".to_string() - })); - assert!(stage_params - .encryption - .options - .contains(&DataLoadingOption { - option_name: "TYPE".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "AWS_SSE_KMS".to_string() - })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_KEY_ID".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "1a2b3c".to_string() + )), + })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_SECRET_KEY".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "4x5y6z".to_string() + )), + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "MASTER_KEY".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "key".to_string() + )), + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "TYPE".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "AWS_SSE_KMS".to_string() + )), + })); } _ => unreachable!(), }; @@ -1854,7 +2150,7 @@ fn test_create_stage_with_directory_table_params() { let sql = concat!( "CREATE OR REPLACE STAGE my_ext_stage ", "URL='s3://load/files/' ", - "DIRECTORY=(ENABLE=TRUE REFRESH_ON_CREATE=FALSE NOTIFICATION_INTEGRATION='some-string')" + "DIRECTORY=(ENABLE=true REFRESH_ON_CREATE=false NOTIFICATION_INTEGRATION='some-string')" ); match snowflake().verified_stmt(sql) { @@ -1862,20 +2158,19 @@ fn test_create_stage_with_directory_table_params() { directory_table_params, .. } => { - assert!(directory_table_params.options.contains(&DataLoadingOption { + assert!(directory_table_params.options.contains(&KeyValueOption { option_name: "ENABLE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, - value: "TRUE".to_string() + option_value: KeyValueOptionKind::Single(Value::Boolean(true)), })); - assert!(directory_table_params.options.contains(&DataLoadingOption { + assert!(directory_table_params.options.contains(&KeyValueOption { option_name: "REFRESH_ON_CREATE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, - value: "FALSE".to_string() + option_value: KeyValueOptionKind::Single(Value::Boolean(false)), })); - assert!(directory_table_params.options.contains(&DataLoadingOption { + assert!(directory_table_params.options.contains(&KeyValueOption { option_name: "NOTIFICATION_INTEGRATION".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "some-string".to_string() + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "some-string".to_string() + )), })); } _ => unreachable!(), @@ -1893,20 +2188,19 @@ fn test_create_stage_with_file_format() { match snowflake_without_unescape().verified_stmt(sql) { Statement::CreateStage { file_format, .. } => { - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "COMPRESSION".to_string(), - option_type: DataLoadingOptionType::ENUM, - value: "AUTO".to_string() + option_value: KeyValueOptionKind::Single(Value::Placeholder("AUTO".to_string())), })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "BINARY_FORMAT".to_string(), - option_type: DataLoadingOptionType::ENUM, - value: "HEX".to_string() + option_value: KeyValueOptionKind::Single(Value::Placeholder("HEX".to_string())), })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "ESCAPE".to_string(), - option_type: DataLoadingOptionType::STRING, - value: r#"\\"#.to_string() + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + r#"\\"#.to_string() + )), })); } _ => unreachable!(), @@ -1922,19 +2216,19 @@ fn test_create_stage_with_copy_options() { let sql = concat!( "CREATE OR REPLACE STAGE my_ext_stage ", "URL='s3://load/files/' ", - "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=TRUE)" + "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=true)" ); match snowflake().verified_stmt(sql) { Statement::CreateStage { copy_options, .. } => { - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "ON_ERROR".to_string(), - option_type: DataLoadingOptionType::ENUM, - value: "CONTINUE".to_string() + option_value: KeyValueOptionKind::Single(Value::Placeholder( + "CONTINUE".to_string() + )), })); - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "FORCE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, - value: "TRUE".to_string() + option_value: KeyValueOptionKind::Single(Value::Boolean(true)), })); } _ => unreachable!(), @@ -1950,20 +2244,25 @@ fn test_copy_into() { ); match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { + kind, into, - from_stage, + from_obj, files, pattern, validation_mode, .. } => { + assert_eq!(kind, CopyIntoSnowflakeKind::Table); assert_eq!( into, - ObjectName(vec![Ident::new("my_company"), Ident::new("emp_basic")]) + ObjectName::from(vec![Ident::new("my_company"), Ident::new("emp_basic")]) ); assert_eq!( - from_stage, - ObjectName(vec![Ident::with_quote('\'', "gcs://mybucket/./../a.csv")]) + from_obj, + Some(ObjectName::from(vec![Ident::with_quote( + '\'', + "gcs://mybucket/./../a.csv" + )])) ); assert!(files.is_none()); assert!(pattern.is_none()); @@ -1972,6 +2271,60 @@ fn test_copy_into() { _ => unreachable!(), }; assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + let sql = concat!("COPY INTO 's3://a/b/c/data.parquet' ", "FROM db.sc.tbl ", "PARTITION BY ('date=' || to_varchar(dt, 'YYYY-MM-DD') || '/hour=' || to_varchar(date_part(hour, ts)))"); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + kind, + into, + from_obj, + from_query, + partition, + .. + } => { + assert_eq!(kind, CopyIntoSnowflakeKind::Location); + assert_eq!( + into, + ObjectName::from(vec![Ident::with_quote('\'', "s3://a/b/c/data.parquet")]) + ); + assert_eq!( + from_obj, + Some(ObjectName::from(vec![ + Ident::new("db"), + Ident::new("sc"), + Ident::new("tbl") + ])) + ); + assert!(from_query.is_none()); + assert!(partition.is_some()); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + let sql = concat!( + "COPY INTO 's3://a/b/c/data.parquet' ", + "FROM (SELECT * FROM tbl)" + ); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + kind, + into, + from_obj, + from_query, + .. + } => { + assert_eq!(kind, CopyIntoSnowflakeKind::Location); + assert_eq!( + into, + ObjectName::from(vec![Ident::with_quote('\'', "s3://a/b/c/data.parquet")]) + ); + assert!(from_query.is_some()); + assert!(from_obj.is_none()); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); } #[test] @@ -1987,52 +2340,47 @@ fn test_copy_into_with_stage_params() { match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { - from_stage, + from_obj, stage_params, .. } => { //assert_eq!("s3://load/files/", stage_params.url.unwrap()); assert_eq!( - from_stage, - ObjectName(vec![Ident::with_quote('\'', "s3://load/files/")]) + from_obj, + Some(ObjectName::from(vec![Ident::with_quote( + '\'', + "s3://load/files/" + )])) ); assert_eq!("myint", stage_params.storage_integration.unwrap()); assert_eq!( "", stage_params.endpoint.unwrap() ); - assert!(stage_params - .credentials - .options - .contains(&DataLoadingOption { - option_name: "AWS_KEY_ID".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "1a2b3c".to_string() - })); - assert!(stage_params - .credentials - .options - .contains(&DataLoadingOption { - option_name: "AWS_SECRET_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "4x5y6z".to_string() - })); - assert!(stage_params - .encryption - .options - .contains(&DataLoadingOption { - option_name: "MASTER_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "key".to_string() - })); - assert!(stage_params - .encryption - .options - .contains(&DataLoadingOption { - option_name: "TYPE".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "AWS_SSE_KMS".to_string() - })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_KEY_ID".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "1a2b3c".to_string() + )), + })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_SECRET_KEY".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "4x5y6z".to_string() + )), + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "MASTER_KEY".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "key".to_string() + )), + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "TYPE".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + "AWS_SSE_KMS".to_string() + )), + })); } _ => unreachable!(), }; @@ -2047,13 +2395,16 @@ fn test_copy_into_with_stage_params() { match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { - from_stage, + from_obj, stage_params, .. } => { assert_eq!( - from_stage, - ObjectName(vec![Ident::with_quote('\'', "s3://load/files/")]) + from_obj, + Some(ObjectName::from(vec![Ident::with_quote( + '\'', + "s3://load/files/" + )])) ); assert_eq!("myint", stage_params.storage_integration.unwrap()); } @@ -2076,13 +2427,13 @@ fn test_copy_into_with_files_and_pattern_and_verification() { files, pattern, validation_mode, - from_stage_alias, + from_obj_alias, .. } => { assert_eq!(files.unwrap(), vec!["file1.json", "file2.json"]); assert_eq!(pattern.unwrap(), ".*employees0[1-5].csv.gz"); assert_eq!(validation_mode.unwrap(), "RETURN_7_ROWS"); - assert_eq!(from_stage_alias.unwrap(), Ident::new("some_alias")); + assert_eq!(from_obj_alias.unwrap(), Ident::new("some_alias")); } _ => unreachable!(), } @@ -2093,7 +2444,7 @@ fn test_copy_into_with_files_and_pattern_and_verification() { fn test_copy_into_with_transformations() { let sql = concat!( "COPY INTO my_company.emp_basic FROM ", - "(SELECT t1.$1:st AS st, $1:index, t2.$1 FROM @schema.general_finished AS T) ", + "(SELECT t1.$1:st AS st, $1:index, t2.$1, 4, '5' AS const_str FROM @schema.general_finished AS T) ", "FILES = ('file1.json', 'file2.json') ", "PATTERN = '.*employees0[1-5].csv.gz' ", "VALIDATION_MODE = RETURN_7_ROWS" @@ -2101,45 +2452,68 @@ fn test_copy_into_with_transformations() { match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { - from_stage, + from_obj, from_transformations, .. } => { assert_eq!( - from_stage, - ObjectName(vec![Ident::new("@schema"), Ident::new("general_finished")]) + from_obj, + Some(ObjectName::from(vec![ + Ident::new("@schema"), + Ident::new("general_finished") + ])) ); assert_eq!( from_transformations.as_ref().unwrap()[0], - StageLoadSelectItem { + StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { alias: Some(Ident::new("t1")), file_col_num: 1, element: Some(Ident::new("st")), item_as: Some(Ident::new("st")) - } + }) ); assert_eq!( from_transformations.as_ref().unwrap()[1], - StageLoadSelectItem { + StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { alias: None, file_col_num: 1, element: Some(Ident::new("index")), item_as: None - } + }) ); assert_eq!( from_transformations.as_ref().unwrap()[2], - StageLoadSelectItem { + StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { alias: Some(Ident::new("t2")), file_col_num: 1, element: None, item_as: None - } + }) + ); + assert_eq!( + from_transformations.as_ref().unwrap()[3], + StageLoadSelectItemKind::SelectItem(SelectItem::UnnamedExpr(Expr::Value( + Value::Number("4".parse().unwrap(), false).into() + ))) + ); + assert_eq!( + from_transformations.as_ref().unwrap()[4], + StageLoadSelectItemKind::SelectItem(SelectItem::ExprWithAlias { + expr: Expr::Value(Value::SingleQuotedString("5".parse().unwrap()).into()), + alias: Ident::new("const_str".to_string()) + }) ); } _ => unreachable!(), } assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + // Test optional AS keyword to denote an alias for the stage + let sql1 = concat!( + "COPY INTO my_company.emp_basic FROM ", + "(SELECT t1.$1:st AS st, $1:index, t2.$1, 4, '5' AS const_str FROM @schema.general_finished T) " + ); + snowflake().parse_sql_statements(sql1).unwrap(); } #[test] @@ -2154,20 +2528,19 @@ fn test_copy_into_file_format() { match snowflake_without_unescape().verified_stmt(sql) { Statement::CopyIntoSnowflake { file_format, .. } => { - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "COMPRESSION".to_string(), - option_type: DataLoadingOptionType::ENUM, - value: "AUTO".to_string() + option_value: KeyValueOptionKind::Single(Value::Placeholder("AUTO".to_string())), })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "BINARY_FORMAT".to_string(), - option_type: DataLoadingOptionType::ENUM, - value: "HEX".to_string() + option_value: KeyValueOptionKind::Single(Value::Placeholder("HEX".to_string())), })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "ESCAPE".to_string(), - option_type: DataLoadingOptionType::STRING, - value: r#"\\"#.to_string() + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + r#"\\"#.to_string() + )), })); } _ => unreachable!(), @@ -2176,6 +2549,40 @@ fn test_copy_into_file_format() { snowflake_without_unescape().verified_stmt(sql).to_string(), sql ); + + // Test commas in file format + let sql = concat!( + "COPY INTO my_company.emp_basic ", + "FROM 'gcs://mybucket/./../a.csv' ", + "FILES = ('file1.json', 'file2.json') ", + "PATTERN = '.*employees0[1-5].csv.gz' ", + r#"FILE_FORMAT=(COMPRESSION=AUTO, BINARY_FORMAT=HEX, ESCAPE='\\')"# + ); + + match snowflake_without_unescape() + .parse_sql_statements(sql) + .unwrap() + .first() + .unwrap() + { + Statement::CopyIntoSnowflake { file_format, .. } => { + assert!(file_format.options.contains(&KeyValueOption { + option_name: "COMPRESSION".to_string(), + option_value: KeyValueOptionKind::Single(Value::Placeholder("AUTO".to_string())), + })); + assert!(file_format.options.contains(&KeyValueOption { + option_name: "BINARY_FORMAT".to_string(), + option_value: KeyValueOptionKind::Single(Value::Placeholder("HEX".to_string())), + })); + assert!(file_format.options.contains(&KeyValueOption { + option_name: "ESCAPE".to_string(), + option_value: KeyValueOptionKind::Single(Value::SingleQuotedString( + r#"\\"#.to_string() + )), + })); + } + _ => unreachable!(), + } } #[test] @@ -2185,20 +2592,20 @@ fn test_copy_into_copy_options() { "FROM 'gcs://mybucket/./../a.csv' ", "FILES = ('file1.json', 'file2.json') ", "PATTERN = '.*employees0[1-5].csv.gz' ", - "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=TRUE)" + "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=true)" ); match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { copy_options, .. } => { - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "ON_ERROR".to_string(), - option_type: DataLoadingOptionType::ENUM, - value: "CONTINUE".to_string() + option_value: KeyValueOptionKind::Single(Value::Placeholder( + "CONTINUE".to_string() + )), })); - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "FORCE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, - value: "TRUE".to_string() + option_value: KeyValueOptionKind::Single(Value::Boolean(true)), })); } _ => unreachable!(), @@ -2207,37 +2614,55 @@ fn test_copy_into_copy_options() { } #[test] -fn test_snowflake_stage_object_names() { - let allowed_formatted_names = [ - "my_company.emp_basic", - "@namespace.%table_name", - "@namespace.%table_name/path", - "@namespace.stage_name/path", - "@~/path", - ]; +fn test_snowflake_stage_object_names_into_location() { let mut allowed_object_names = [ - ObjectName(vec![Ident::new("my_company"), Ident::new("emp_basic")]), - ObjectName(vec![Ident::new("@namespace"), Ident::new("%table_name")]), - ObjectName(vec![ + ObjectName::from(vec![Ident::new("@namespace"), Ident::new("%table_name")]), + ObjectName::from(vec![ Ident::new("@namespace"), Ident::new("%table_name/path"), ]), - ObjectName(vec![ + ObjectName::from(vec![ Ident::new("@namespace"), Ident::new("stage_name/path"), ]), - ObjectName(vec![Ident::new("@~/path")]), + ObjectName::from(vec![Ident::new("@~/path")]), ]; - for it in allowed_formatted_names + let allowed_names_into_location = [ + "@namespace.%table_name", + "@namespace.%table_name/path", + "@namespace.stage_name/path", + "@~/path", + ]; + for it in allowed_names_into_location .iter() .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) + } + _ => unreachable!(), + } + } +} + +#[test] +fn test_snowflake_stage_object_names_into_table() { + let mut allowed_object_names = [ + ObjectName::from(vec![Ident::new("my_company"), Ident::new("emp_basic")]), + ObjectName::from(vec![Ident::new("emp_basic")]), + ]; + + let allowed_names_into_table = ["my_company.emp_basic", "emp_basic"]; + for it in allowed_names_into_table + .iter() + .zip(allowed_object_names.iter_mut()) + { + let (formatted_name, object_name) = it; + 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) @@ -2252,13 +2677,37 @@ fn test_snowflake_copy_into() { let sql = "COPY INTO a.b FROM @namespace.stage_name"; assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); match snowflake().verified_stmt(sql) { - Statement::CopyIntoSnowflake { - into, from_stage, .. - } => { - assert_eq!(into, ObjectName(vec![Ident::new("a"), Ident::new("b")])); + 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") + ])) + ) + } + _ => 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-x"; + 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_stage, - ObjectName(vec![Ident::new("@namespace"), Ident::new("stage_name")]) + from_obj, + Some(ObjectName::from(vec![ + Ident::new("@namespace"), + Ident::new("stage_name/x@x~x%x+/20250723_data-x") + ])) ) } _ => unreachable!(), @@ -2270,19 +2719,20 @@ fn test_snowflake_copy_into_stage_name_ends_with_parens() { let sql = "COPY INTO SCHEMA.SOME_MONITORING_SYSTEM FROM (SELECT t.$1:st AS st FROM @schema.general_finished)"; assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); match snowflake().verified_stmt(sql) { - Statement::CopyIntoSnowflake { - into, from_stage, .. - } => { + Statement::CopyIntoSnowflake { into, from_obj, .. } => { assert_eq!( into, - ObjectName(vec![ + ObjectName::from(vec![ Ident::new("SCHEMA"), Ident::new("SOME_MONITORING_SYSTEM") ]) ); assert_eq!( - from_stage, - ObjectName(vec![Ident::new("@schema"), Ident::new("general_finished")]) + from_obj, + Some(ObjectName::from(vec![ + Ident::new("@schema"), + Ident::new("general_finished") + ])) ) } _ => unreachable!(), @@ -2298,10 +2748,14 @@ fn test_snowflake_trim() { let select = snowflake().verified_only_select(sql_only_select); assert_eq!( &Expr::Trim { - expr: Box::new(Expr::Value(Value::SingleQuotedString("xyz".to_owned()))), + 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()))]), + trim_characters: Some(vec![Expr::Value( + (Value::SingleQuotedString("a".to_owned())).with_empty_span() + )]), }, expr_from_projection(only(&select.projection)) ); @@ -2319,7 +2773,7 @@ fn test_number_placeholder() { let sql_only_select = "SELECT :1"; let select = snowflake().verified_only_select(sql_only_select); assert_eq!( - &Expr::Value(Value::Placeholder(":1".into())), + &Expr::Value((Value::Placeholder(":1".into())).with_empty_span()), expr_from_projection(only(&select.projection)) ); @@ -2465,7 +2919,7 @@ fn parse_comma_outer_join() { "myudf", [Expr::UnaryOp { op: UnaryOperator::Plus, - expr: Box::new(Expr::Value(number("42"))) + expr: Box::new(Expr::value(number("42"))) }] )), }) @@ -2650,6 +3104,20 @@ fn asof_joins() { "ON s.state = p.state ", "ORDER BY s.observed", )); + + // Test without explicit aliases + #[rustfmt::skip] + snowflake_and_generic().verified_query(concat!( + "SELECT * ", + "FROM snowtime ", + "ASOF JOIN raintime ", + "MATCH_CONDITION (snowtime.observed >= raintime.observed) ", + "ON snowtime.state = raintime.state ", + "ASOF JOIN preciptime ", + "MATCH_CONDITION (showtime.observed >= preciptime.observed) ", + "ON showtime.state = preciptime.state ", + "ORDER BY showtime.observed", + )); } #[test] @@ -2695,16 +3163,16 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + snowflake().verified_stmt(&format!("USE {object_name}")), + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_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(), )]))) @@ -2715,8 +3183,10 @@ 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)), - Statement::Use(Use::Object(ObjectName(vec![ + 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") ]))) @@ -2725,7 +3195,7 @@ fn parse_use() { // Test double identifier without quotes assert_eq!( snowflake().verified_stmt("USE mydb.my_schema"), - Statement::Use(Use::Object(ObjectName(vec![ + Statement::Use(Use::Object(ObjectName::from(vec![ Ident::new("mydb"), Ident::new("my_schema") ]))) @@ -2734,36 +3204,38 @@ 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)), - Statement::Use(Use::Database(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Schema(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Schema(ObjectName(vec![ + 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)), - Statement::Use(Use::Role(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Warehouse(ObjectName(vec![Ident::with_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(), )]))) @@ -2799,7 +3271,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); } } @@ -2808,8 +3280,8 @@ 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) { - Statement::CreateView { name, columns, .. } => { + match snowflake().verified_stmt(sql) { + Statement::CreateView(CreateView { name, columns, .. }) => { assert_eq!(name.to_string(), "v"); assert_eq!( columns, @@ -2817,7 +3289,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"), @@ -2859,6 +3333,7 @@ fn test_parentheses_overflow() { #[test] fn test_show_databases() { snowflake().verified_stmt("SHOW DATABASES"); + snowflake().verified_stmt("SHOW TERSE DATABASES"); snowflake().verified_stmt("SHOW DATABASES HISTORY"); snowflake().verified_stmt("SHOW DATABASES LIKE '%abc%'"); snowflake().verified_stmt("SHOW DATABASES STARTS WITH 'demo_db'"); @@ -2871,6 +3346,7 @@ fn test_show_databases() { #[test] fn test_parse_show_schemas() { snowflake().verified_stmt("SHOW SCHEMAS"); + snowflake().verified_stmt("SHOW TERSE SCHEMAS"); snowflake().verified_stmt("SHOW SCHEMAS IN ACCOUNT"); snowflake().verified_stmt("SHOW SCHEMAS IN ACCOUNT abc"); snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE"); @@ -2880,9 +3356,51 @@ fn test_parse_show_schemas() { snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); } +#[test] +fn test_parse_show_objects() { + snowflake().verified_stmt("SHOW OBJECTS"); + snowflake().verified_stmt("SHOW OBJECTS IN abc"); + snowflake().verified_stmt("SHOW OBJECTS LIKE '%test%' IN abc"); + snowflake().verified_stmt("SHOW OBJECTS IN ACCOUNT"); + snowflake().verified_stmt("SHOW OBJECTS IN DATABASE"); + snowflake().verified_stmt("SHOW OBJECTS IN DATABASE abc"); + snowflake().verified_stmt("SHOW OBJECTS IN SCHEMA"); + snowflake().verified_stmt("SHOW OBJECTS IN SCHEMA abc"); + snowflake().verified_stmt("SHOW TERSE OBJECTS"); + snowflake().verified_stmt("SHOW TERSE OBJECTS IN abc"); + snowflake().verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc"); + snowflake().verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc STARTS WITH 'b'"); + snowflake().verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc STARTS WITH 'b' LIMIT 10"); + snowflake() + .verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc STARTS WITH 'b' LIMIT 10 FROM 'x'"); + match snowflake().verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc") { + Statement::ShowObjects(ShowObjects { + terse, + show_options, + }) => { + assert!(terse); + let name = match show_options.show_in { + Some(ShowStatementIn { + parent_name: Some(val), + .. + }) => val.to_string(), + _ => unreachable!(), + }; + assert_eq!("abc", name); + let like = match show_options.filter_position { + Some(ShowStatementFilterPosition::Infix(ShowStatementFilter::Like(val))) => val, + _ => unreachable!(), + }; + assert_eq!("%test%", like); + } + _ => unreachable!(), + } +} + #[test] fn test_parse_show_tables() { snowflake().verified_stmt("SHOW TABLES"); + snowflake().verified_stmt("SHOW TERSE TABLES"); snowflake().verified_stmt("SHOW TABLES IN ACCOUNT"); snowflake().verified_stmt("SHOW TABLES IN DATABASE"); snowflake().verified_stmt("SHOW TABLES IN DATABASE xyz"); @@ -2905,6 +3423,7 @@ fn test_parse_show_tables() { #[test] fn test_show_views() { snowflake().verified_stmt("SHOW VIEWS"); + snowflake().verified_stmt("SHOW TERSE VIEWS"); snowflake().verified_stmt("SHOW VIEWS IN ACCOUNT"); snowflake().verified_stmt("SHOW VIEWS IN DATABASE"); snowflake().verified_stmt("SHOW VIEWS IN DATABASE xyz"); @@ -3001,7 +3520,7 @@ fn parse_ls_and_rm() { .verified_stmt("LIST @SNOWFLAKE_KAFKA_CONNECTOR_externalDataLakeSnowflakeConnector_STAGE_call_tracker_stream/"); match statement { Statement::List(command) => { - assert_eq!(command.stage, ObjectName(vec!["@SNOWFLAKE_KAFKA_CONNECTOR_externalDataLakeSnowflakeConnector_STAGE_call_tracker_stream/".into()])); + assert_eq!(command.stage, ObjectName::from(vec!["@SNOWFLAKE_KAFKA_CONNECTOR_externalDataLakeSnowflakeConnector_STAGE_call_tracker_stream/".into()])); assert!(command.pattern.is_none()); } _ => unreachable!(), @@ -3013,7 +3532,7 @@ fn parse_ls_and_rm() { Statement::Remove(command) => { assert_eq!( command.stage, - ObjectName(vec!["@my_csv_stage/analysis/".into()]) + ObjectName::from(vec!["@my_csv_stage/analysis/".into()]) ); assert_eq!(command.pattern, Some(".*data_0.*".to_string())); } @@ -3021,4 +3540,1133 @@ fn parse_ls_and_rm() { }; snowflake().verified_stmt(r#"LIST @"STAGE_WITH_QUOTES""#); + // Semi-colon after stage name - should terminate the stage name + snowflake() + .parse_sql_statements("LIST @db1.schema1.stage1/dir1/;") + .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", "SORT"]; + for kw in unreserved_kws { + snowflake() + .one_statement_parses_to(&format!("SELECT 1 {kw}"), &format!("SELECT 1 AS {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()); + } + + // 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] +fn test_timetravel_at_before() { + snowflake().verified_only_select("SELECT * FROM tbl AT(TIMESTAMP => '2024-12-15 00:00:00')"); + snowflake() + .verified_only_select("SELECT * FROM tbl BEFORE(TIMESTAMP => '2024-12-15 00:00:00')"); +} + +#[test] +fn test_grant_account_global_privileges() { + let privileges = vec![ + "ALL", + "ALL PRIVILEGES", + "ATTACH POLICY", + "AUDIT", + "BIND SERVICE ENDPOINT", + "IMPORT SHARE", + "OVERRIDE SHARE RESTRICTIONS", + "PURCHASE DATA EXCHANGE LISTING", + "RESOLVE ALL", + "READ SESSION", + ]; + let with_grant_options = vec!["", " WITH GRANT OPTION"]; + + for p in &privileges { + for wgo in &with_grant_options { + let sql = format!("GRANT {p} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let create_object_types = vec![ + "ACCOUNT", + "APPLICATION", + "APPLICATION PACKAGE", + "COMPUTE POOL", + "DATA EXCHANGE LISTING", + "DATABASE", + "EXTERNAL VOLUME", + "FAILOVER GROUP", + "INTEGRATION", + "NETWORK POLICY", + "ORGANIZATION LISTING", + "REPLICATION GROUP", + "ROLE", + "SHARE", + "USER", + "WAREHOUSE", + ]; + for t in &create_object_types { + for wgo in &with_grant_options { + let sql = format!("GRANT CREATE {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let apply_types = vec![ + "AGGREGATION POLICY", + "AUTHENTICATION POLICY", + "JOIN POLICY", + "MASKING POLICY", + "PACKAGES POLICY", + "PASSWORD POLICY", + "PROJECTION POLICY", + "ROW ACCESS POLICY", + "SESSION POLICY", + "TAG", + ]; + for t in &apply_types { + for wgo in &with_grant_options { + let sql = format!("GRANT APPLY {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let execute_types = vec![ + "ALERT", + "DATA METRIC FUNCTION", + "MANAGED ALERT", + "MANAGED TASK", + "TASK", + ]; + for t in &execute_types { + for wgo in &with_grant_options { + let sql = format!("GRANT EXECUTE {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let manage_types = vec![ + "ACCOUNT SUPPORT CASES", + "EVENT SHARING", + "GRANTS", + "LISTING AUTO FULFILLMENT", + "ORGANIZATION SUPPORT CASES", + "USER SUPPORT CASES", + "WAREHOUSES", + ]; + for t in &manage_types { + for wgo in &with_grant_options { + let sql = format!("GRANT MANAGE {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let monitor_types = vec!["EXECUTION", "SECURITY", "USAGE"]; + for t in &monitor_types { + for wgo in &with_grant_options { + let sql = format!("GRANT MONITOR {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } +} + +#[test] +fn test_grant_account_object_privileges() { + let privileges = vec![ + "ALL", + "ALL PRIVILEGES", + "APPLYBUDGET", + "MODIFY", + "MONITOR", + "USAGE", + "OPERATE", + ]; + + let objects_types = vec![ + "USER", + "RESOURCE MONITOR", + "WAREHOUSE", + "COMPUTE POOL", + "DATABASE", + "INTEGRATION", + "CONNECTION", + "FAILOVER GROUP", + "REPLICATION GROUP", + "EXTERNAL VOLUME", + ]; + + let with_grant_options = vec!["", " WITH GRANT OPTION"]; + + for t in &objects_types { + for p in &privileges { + for wgo in &with_grant_options { + let sql = format!("GRANT {p} ON {t} obj1 TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + } +} + +#[test] +fn test_grant_role_to() { + snowflake_and_generic().verified_stmt("GRANT ROLE r1 TO ROLE r2"); + snowflake_and_generic().verified_stmt("GRANT ROLE r1 TO USER u1"); +} + +#[test] +fn test_grant_database_role_to() { + snowflake_and_generic().verified_stmt("GRANT DATABASE ROLE r1 TO ROLE r2"); + snowflake_and_generic().verified_stmt("GRANT DATABASE ROLE db1.sc1.r1 TO ROLE db1.sc1.r2"); +} + +#[test] +fn test_alter_session() { + assert_eq!( + snowflake() + .parse_sql_statements("ALTER SESSION SET") + .unwrap_err() + .to_string(), + "sql parser error: expected at least one option" + ); + assert_eq!( + snowflake() + .parse_sql_statements("ALTER SESSION UNSET") + .unwrap_err() + .to_string(), + "sql parser error: expected at least one option" + ); + + snowflake().one_statement_parses_to( + "ALTER SESSION SET AUTOCOMMIT=TRUE", + "ALTER SESSION SET AUTOCOMMIT=true", + ); + snowflake().verified_stmt("ALTER SESSION SET AUTOCOMMIT=false QUERY_TAG='tag'"); + snowflake().verified_stmt("ALTER SESSION UNSET AUTOCOMMIT"); + snowflake().verified_stmt("ALTER SESSION UNSET AUTOCOMMIT, QUERY_TAG"); + snowflake().one_statement_parses_to( + "ALTER SESSION SET A=false, B='tag';", + "ALTER SESSION SET A=false B='tag'", + ); + snowflake().one_statement_parses_to( + "ALTER SESSION SET A=true \nB='tag'", + "ALTER SESSION SET A=true B='tag'", + ); + snowflake().one_statement_parses_to("ALTER SESSION UNSET a\nB", "ALTER SESSION UNSET a, B"); +} + +#[test] +fn test_alter_session_followed_by_statement() { + let stmts = snowflake() + .parse_sql_statements("ALTER SESSION SET QUERY_TAG='hello'; SELECT 42") + .unwrap(); + match stmts[..] { + [Statement::AlterSession { .. }, Statement::Query { .. }] => {} + _ => panic!("Unexpected statements: {stmts:?}"), + } +} + +#[test] +fn test_nested_join_without_parentheses() { + let query = "SELECT DISTINCT p.product_id FROM orders AS o INNER JOIN customers AS c INNER JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o INNER JOIN (customers AS c INNER JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + })), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); + + let query = "SELECT DISTINCT p.product_id FROM orders AS o JOIN customers AS c JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o JOIN (customers AS c JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + })), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); + + let query = "SELECT DISTINCT p.product_id FROM orders AS o LEFT JOIN customers AS c LEFT JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o LEFT JOIN (customers AS c LEFT JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::Left(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + })), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::Left(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); + + let query = "SELECT DISTINCT p.product_id FROM orders AS o RIGHT JOIN customers AS c RIGHT JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o RIGHT JOIN (customers AS c RIGHT JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::Right(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + })), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::Right(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); + + let query = "SELECT DISTINCT p.product_id FROM orders AS o FULL JOIN customers AS c FULL JOIN products AS p ON p.customer_id = c.customer_id ON c.order_id = o.order_id"; + assert_eq!( + only( + snowflake() + .verified_only_select_with_canonical(query, "SELECT DISTINCT p.product_id FROM orders AS o FULL JOIN (customers AS c FULL JOIN products AS p ON p.customer_id = c.customer_id) ON c.order_id = o.order_id") + .from + ) + .joins, + vec![Join { + relation: TableFactor::NestedJoin { + table_with_joins: Box::new(TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("customers".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "c".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![Join { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("products".to_string())]), + alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + global: false, + join_operator: JoinOperator::FullOuter(JoinConstraint::On( + Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("p".to_string()), + Ident::new("customer_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("customer_id".to_string()) + ])), + } + )), + }] + }), + alias: None + }, + global: false, + join_operator: JoinOperator::FullOuter(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("c".to_string()), + Ident::new("order_id".to_string()) + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("o".to_string()), + Ident::new("order_id".to_string()) + ])), + })) + }], + ); +} + +#[test] +fn parse_connect_by_root_operator() { + let sql = "SELECT CONNECT_BY_ROOT name AS root_name FROM Tbl1"; + + match snowflake().verified_stmt(sql) { + Statement::Query(query) => { + assert_eq!( + query.body.as_select().unwrap().projection[0], + SelectItem::ExprWithAlias { + expr: Expr::Prefixed { + prefix: Ident::new("CONNECT_BY_ROOT"), + value: Box::new(Expr::Identifier(Ident::new("name"))) + }, + alias: Ident::new("root_name"), + } + ); + } + _ => unreachable!(), + } + + let sql = "SELECT CONNECT_BY_ROOT name FROM Tbl2"; + match snowflake().verified_stmt(sql) { + Statement::Query(query) => { + assert_eq!( + query.body.as_select().unwrap().projection[0], + SelectItem::UnnamedExpr(Expr::Prefixed { + prefix: Ident::new("CONNECT_BY_ROOT"), + value: Box::new(Expr::Identifier(Ident::new("name"))) + }) + ); + } + _ => unreachable!(), + } + + let sql = "SELECT CONNECT_BY_ROOT FROM Tbl2"; + let res = snowflake().parse_sql_statements(sql); + assert_eq!( + res.unwrap_err().to_string(), + "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_timestamp_ntz_with_precision() { + snowflake().verified_stmt("SELECT CAST('2024-01-01 01:00:00' AS TIMESTAMP_NTZ(1))"); + snowflake().verified_stmt("SELECT CAST('2024-01-01 01:00:00' AS TIMESTAMP_NTZ(9))"); + + let select = + snowflake().verified_only_select("SELECT CAST('2024-01-01 01:00:00' AS TIMESTAMP_NTZ(9))"); + match expr_from_projection(only(&select.projection)) { + Expr::Cast { data_type, .. } => { + assert_eq!(*data_type, DataType::TimestampNtz(Some(9))); + } + _ => unreachable!(), + } +} + +#[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"); +} + +#[test] +fn test_alter_dynamic_table() { + snowflake().verified_stmt("ALTER DYNAMIC TABLE MY_DYNAMIC_TABLE REFRESH"); + snowflake().verified_stmt("ALTER DYNAMIC TABLE my_database.my_schema.my_dynamic_table REFRESH"); + snowflake().verified_stmt("ALTER DYNAMIC TABLE my_dyn_table SUSPEND"); + snowflake().verified_stmt("ALTER DYNAMIC TABLE my_dyn_table RESUME"); } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index edd1365f4..f1f6cf49b 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -166,7 +166,7 @@ fn parse_create_virtual_table() { fn parse_create_view_temporary_if_not_exists() { let sql = "CREATE TEMPORARY VIEW IF NOT EXISTS myschema.myview AS SELECT foo FROM bar"; match sqlite_and_generic().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, columns, query, @@ -179,7 +179,7 @@ fn parse_create_view_temporary_if_not_exists() { if_not_exists, temporary, .. - } => { + }) => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -214,14 +214,17 @@ fn parse_create_table_auto_increment() { vec![ColumnDef { name: "bar".into(), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None - }, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + }), }, ColumnOptionDef { name: None, @@ -243,14 +246,17 @@ fn parse_create_table_primary_key_asc_desc() { let expected_column_def = |kind| ColumnDef { name: "bar".into(), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], characteristics: None, - }, + }), }, ColumnOptionDef { name: None, @@ -286,13 +292,11 @@ fn parse_create_sqlite_quote() { ColumnDef { name: Ident::with_quote('"', "KEY"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ColumnDef { name: Ident::with_quote('[', "INDEX"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ], @@ -328,7 +332,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!( @@ -373,7 +377,9 @@ fn test_placeholder() { let ast = sqlite().verified_only_select(sql); assert_eq!( ast.projection[0], - UnnamedExpr(Expr::Value(Value::Placeholder("@xxx".into()))), + UnnamedExpr(Expr::Value( + (Value::Placeholder("@xxx".into())).with_empty_span() + )), ); } @@ -412,13 +418,13 @@ 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!( select.projection, vec![SelectItem::UnnamedExpr(Expr::Function(Function { - name: ObjectName(vec![Ident::new(func_name)]), + name: ObjectName::from(vec![Ident::new(func_name)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -446,11 +452,15 @@ 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, - database_file_name: Expr::Value(Value::SingleQuotedString(literal_name)), + database_file_name: + Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(literal_name), + span: _, + }), database: true, } => { assert_eq!(schema_name.value, "test"); @@ -465,26 +475,27 @@ fn parse_update_tuple_row_values() { // See https://github.com/sqlparser-rs/sqlparser-rs/issues/1311 assert_eq!( sqlite().verified_stmt("UPDATE x SET (a, b) = (1, 2)"), - Statement::Update { + Statement::Update(Update { or: None, assignments: vec![Assignment { target: AssignmentTarget::Tuple(vec![ - ObjectName(vec![Ident::new("a"),]), - ObjectName(vec![Ident::new("b"),]), + ObjectName::from(vec![Ident::new("a"),]), + ObjectName::from(vec![Ident::new("b"),]), ]), value: Expr::Tuple(vec![ - Expr::Value(Value::Number("1".parse().unwrap(), false)), - Expr::Value(Value::Number("2".parse().unwrap(), false)) + Expr::Value((Value::Number("1".parse().unwrap(), false)).with_empty_span()), + Expr::Value((Value::Number("2".parse().unwrap(), false)).with_empty_span()) ]) }], selection: None, table: TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("x")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("x")])), joins: vec![], }, from: None, - returning: None - } + returning: None, + limit: None + }) ); } @@ -522,23 +533,6 @@ fn parse_start_transaction_with_modifier() { sqlite_and_generic().verified_stmt("BEGIN DEFERRED"); sqlite_and_generic().verified_stmt("BEGIN IMMEDIATE"); sqlite_and_generic().verified_stmt("BEGIN EXCLUSIVE"); - - let unsupported_dialects = all_dialects_except(|d| d.supports_start_transaction_modifier()); - let res = unsupported_dialects.parse_sql_statements("BEGIN DEFERRED"); - assert_eq!( - ParserError::ParserError("Expected: end of statement, found: DEFERRED".to_string()), - res.unwrap_err(), - ); - let res = unsupported_dialects.parse_sql_statements("BEGIN IMMEDIATE"); - assert_eq!( - ParserError::ParserError("Expected: end of statement, found: IMMEDIATE".to_string()), - res.unwrap_err(), - ); - let res = unsupported_dialects.parse_sql_statements("BEGIN EXCLUSIVE"); - assert_eq!( - ParserError::ParserError("Expected: end of statement, found: EXCLUSIVE".to_string()), - res.unwrap_err(), - ); } #[test] @@ -551,7 +545,12 @@ fn test_dollar_identifier_as_placeholder() { Expr::BinaryOp { op, left, right } => { assert_eq!(op, BinaryOperator::Eq); assert_eq!(left, Box::new(Expr::Identifier(Ident::new("id")))); - assert_eq!(right, Box::new(Expr::Value(Placeholder("$id".to_string())))); + assert_eq!( + right, + Box::new(Expr::Value( + (Placeholder("$id".to_string())).with_empty_span() + )) + ); } _ => unreachable!(), } @@ -561,12 +560,343 @@ fn test_dollar_identifier_as_placeholder() { Expr::BinaryOp { op, left, right } => { assert_eq!(op, BinaryOperator::Eq); assert_eq!(left, Box::new(Expr::Identifier(Ident::new("id")))); - assert_eq!(right, Box::new(Expr::Value(Placeholder("$$".to_string())))); + assert_eq!( + right, + Box::new(Expr::Value( + (Placeholder("$$".to_string())).with_empty_span() + )) + ); } _ => unreachable!(), } } +#[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(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!(), + } +} + +#[test] +fn test_create_trigger() { + let statement1 = "CREATE TRIGGER trg_inherit_asset_models AFTER INSERT ON assets FOR EACH ROW BEGIN INSERT INTO users (name) SELECT pam.name FROM users AS pam; END"; + + match sqlite().verified_stmt(statement1) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "trg_inherit_asset_models"); + assert_eq!(period, Some(TriggerPeriod::After)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "assets"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert_eq!( + trigger_object, + Some(TriggerObjectKind::ForEach(TriggerObject::Row)) + ); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // Here we check that the variant of CREATE TRIGGER that omits the `FOR EACH ROW` clause, + // which in SQLite may be implicitly assumed, is parsed correctly. + let statement2 = "CREATE TRIGGER log_new_user AFTER INSERT ON users BEGIN INSERT INTO user_log (user_id, action, timestamp) VALUES (NEW.id, 'created', datetime('now')); END"; + + match sqlite().verified_stmt(statement2) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "log_new_user"); + assert_eq!(period, Some(TriggerPeriod::After)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "users"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + let statement3 = "CREATE TRIGGER cleanup_orders AFTER DELETE ON customers BEGIN DELETE FROM orders WHERE customer_id = OLD.id; DELETE FROM invoices WHERE customer_id = OLD.id; END"; + match sqlite().verified_stmt(statement3) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "cleanup_orders"); + assert_eq!(period, Some(TriggerPeriod::After)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Delete]); + assert_eq!(table_name.to_string(), "customers"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + let statement4 = "CREATE TRIGGER trg_before_update BEFORE UPDATE ON products FOR EACH ROW WHEN NEW.price < 0 BEGIN SELECT RAISE(ABORT, 'Price cannot be negative'); END"; + match sqlite().verified_stmt(statement4) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "trg_before_update"); + assert_eq!(period, Some(TriggerPeriod::Before)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Update(Vec::new())]); + assert_eq!(table_name.to_string(), "products"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert_eq!( + trigger_object, + Some(TriggerObjectKind::ForEach(TriggerObject::Row)) + ); + assert!(condition.is_some()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // We test a INSTEAD OF trigger on a view + let statement5 = "CREATE TRIGGER trg_instead_of_insert INSTEAD OF INSERT ON my_view BEGIN INSERT INTO my_table (col1, col2) VALUES (NEW.col1, NEW.col2); END"; + match sqlite().verified_stmt(statement5) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "trg_instead_of_insert"); + assert_eq!(period, Some(TriggerPeriod::InsteadOf)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "my_view"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // We test a temporary trigger + let statement6 = "CREATE TEMPORARY TRIGGER temp_trigger AFTER INSERT ON temp_table BEGIN UPDATE log_table SET count = count + 1; END"; + match sqlite().verified_stmt(statement6) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "temp_trigger"); + assert_eq!(period, Some(TriggerPeriod::After)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "temp_table"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // We test a trigger defined without a period (BEFORE/AFTER/INSTEAD OF) + let statement7 = "CREATE TRIGGER trg_inherit_asset_models INSERT ON assets FOR EACH ROW BEGIN INSERT INTO users (name) SELECT pam.name FROM users AS pam; END"; + sqlite().verified_stmt(statement7); +} + +#[test] +fn test_drop_trigger() { + let statement = "DROP TRIGGER IF EXISTS trg_inherit_asset_models"; + + match sqlite().verified_stmt(statement) { + Statement::DropTrigger(DropTrigger { + if_exists, + trigger_name, + table_name, + option, + }) => { + assert!(if_exists); + assert_eq!(trigger_name.to_string(), "trg_inherit_asset_models"); + assert!(table_name.is_none()); + assert!(option.is_none()); + } + _ => unreachable!("Expected DROP TRIGGER statement"), + } +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) }