diff --git a/Cargo.lock b/Cargo.lock index 6de035f..cfc27b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,7 +47,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -58,7 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -67,6 +67,31 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.5.53" @@ -119,6 +144,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "hashbrown" version = "0.16.1" @@ -147,6 +178,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + [[package]] name = "log" version = "0.4.28" @@ -159,6 +196,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -174,6 +220,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "quote" version = "1.0.42" @@ -183,6 +239,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "recursive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" +dependencies = [ + "recursive-proc-macro-impl", + "stacker", +] + +[[package]] +name = "recursive-proc-macro-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.2" @@ -251,6 +327,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "sqlchisel" version = "1.0.0" @@ -265,11 +347,25 @@ dependencies = [ [[package]] name = "sqlparser" -version = "0.47.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295e9930cd7a97e58ca2a070541a3ca502b17f5d1fa7157376d0fabd85324f25" +checksum = "dbf5ea8d4d7c808e1af1cbabebca9a2abe603bcefc22294c5b95018d53200cb7" dependencies = [ "log", + "recursive", +] + +[[package]] +name = "stacker" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", ] [[package]] @@ -348,6 +444,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -357,6 +462,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index 67ffedb..a9e3044 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,5 @@ anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } toml = "0.8" -sqlparser = "0.47" +sqlparser = "0.61" regex = "1.10" diff --git a/README.md b/README.md index 33e47d3..88e1d56 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ cat query.sql | sqlchisel --stdin --format - Stable formatter behavior: [`docs/format-contract.md`](docs/format-contract.md) - Formatting heuristics (non-contract): [`docs/style-guide.md`](docs/style-guide.md) - Dremio support notes: [`docs/dremio.md`](docs/dremio.md) +- Dremio reference coverage tracker: [`docs/dremio-support-matrix.md`](docs/dremio-support-matrix.md) - Editor integrations: [`docs/editor-integrations.md`](docs/editor-integrations.md) ## Contributing diff --git a/docs/README.md b/docs/README.md index e837289..4ff0542 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,7 @@ This directory separates stable behavior from formatting heuristics and integrat - [`style-guide.md`](style-guide.md): current formatting heuristics and examples (non-contract) - [`dremio.md`](dremio.md): Dremio-specific supported syntax and formatting expectations +- [`dremio-support-matrix.md`](dremio-support-matrix.md): command-by-command Dremio SQL reference coverage tracker ## Integrations and Development diff --git a/docs/contributing.md b/docs/contributing.md index 91f288b..9bf2fa3 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -24,6 +24,11 @@ This guide covers day-to-day development practices for `sqlchisel`. - Keep `docs/repo-cleanup-checklist.md` updated while the docs/module split effort is in progress. - Prefer using `--format`, `--check`, or `--write` locally to mirror user workflows. - For Dremio work, add parser + formatter coverage and tests together. +- For Dremio syntax additions, update all four together: + - `fixtures/dremio/reference-commands/` + - parser tests (`src/parser.rs`) + - formatter tests (`src/format/sql/mod.rs`) + - `docs/dremio-support-matrix.md` ## Code Style diff --git a/docs/dremio-support-matrix.md b/docs/dremio-support-matrix.md new file mode 100644 index 0000000..436f2e8 --- /dev/null +++ b/docs/dremio-support-matrix.md @@ -0,0 +1,106 @@ +# Dremio SQL Reference Coverage Matrix + +This file tracks `sqlchisel` support against the Dremio SQL command pages listed in [`sql-reference-found.md`](../sql-reference-found.md). + +Evaluated on: 2026-02-28 +Reference export timestamp: 2026-02-28T14:20:54.462923+00:00 + +## Status Legend + +- `NATIVE`: Custom Dremio parser + formatter path (`DremioCommand`). +- `AST`: Parsed as SQL AST and formatted through generic SQL statement handling. +- `PARTIAL`: Some forms work, but important syntax from Dremio reference is missing or fallback-ish. +- `UNSUPPORTED`: Representative command syntax fails strict parse today. +- `INDEX`: Reference index/overview page, not a direct SQL command syntax page. + +## Where Formatter Behavior Is Tracked + +- Stable guarantees: [`docs/format-contract.md`](format-contract.md) +- Formatting heuristics: [`docs/style-guide.md`](style-guide.md) +- Dremio notes (high-level): [`docs/dremio.md`](dremio.md) +- Dremio command parser: [`src/parser.rs`](../src/parser.rs) (`parse_dremio_command`) +- SQL statement formatter dispatch: [`src/format/sql/mod.rs`](../src/format/sql/mod.rs) (`format_statement`) +- Dremio command formatter: [`src/format/sql/dremio.rs`](../src/format/sql/dremio.rs) +- Regression fixtures: `fixtures/dremio/{in,expected,out}` + +## Coverage Matrix (57 Dremio Command Pages) + +| # | Dremio Reference Page | Status | Fixture | Notes | +| --- | --- | --- | --- | --- | +| 1 | ALTER BRANCH | NATIVE | `fixtures/dremio/reference-commands/01_alter_branch.sql` | `BranchTag` command path | +| 2 | ALTER FOLDER | NATIVE | `fixtures/dremio/reference-commands/02_alter_folder.sql` | Generic Dremio command path | +| 3 | ALTER PIPE Enterprise | NATIVE | `fixtures/dremio/reference-commands/03_alter_pipe.sql` | `Pipe` command path | +| 4 | ALTER SOURCE | NATIVE | `fixtures/dremio/reference-commands/04_alter_source.sql` | Generic Dremio command path | +| 5 | ALTER SPACE | NATIVE | `fixtures/dremio/reference-commands/05_alter_space.sql` | Generic Dremio command path | +| 6 | ALTER TABLE | NATIVE | `fixtures/dremio/reference-commands/06_alter_table.sql` | Reflection-specific + generic ALTER TABLE path | +| 7 | ALTER TAG | NATIVE | `fixtures/dremio/reference-commands/07_alter_tag.sql` | `BranchTag` command path | +| 8 | ALTER VIEW | NATIVE | `fixtures/dremio/reference-commands/08_alter_view.sql` | Generic Dremio command path | +| 9 | ANALYZE TABLE | NATIVE | `fixtures/dremio/reference-commands/09_analyze_table.sql` | `AnalyzeTable` command path | +| 10 | COPY INTO | NATIVE | `fixtures/dremio/reference-commands/10_copy_into.sql` | Supports both `COPY INTO` and `COPY INTO TABLE` | +| 11 | CREATE BRANCH | NATIVE | `fixtures/dremio/reference-commands/11_create_branch.sql` | `BranchTag` command path | +| 12 | CREATE FOLDER | NATIVE | `fixtures/dremio/reference-commands/12_create_folder.sql` | `CreateFolder` command path | +| 13 | CREATE PIPE Enterprise | NATIVE | `fixtures/dremio/reference-commands/13_create_pipe.sql` | `Pipe` command path | +| 14 | CREATE TABLE | AST | `fixtures/dremio/reference-commands/14_create_table.sql` | SQL AST path | +| 15 | CREATE TABLE AS | AST | `fixtures/dremio/reference-commands/15_create_table_as.sql` | SQL AST path | +| 16 | CREATE TAG | NATIVE | `fixtures/dremio/reference-commands/16_create_tag.sql` | `BranchTag` command path | +| 17 | CREATE VIEW | AST | `fixtures/dremio/reference-commands/17_create_view.sql` | SQL AST path | +| 18 | DELETE | AST | `fixtures/dremio/reference-commands/18_delete.sql` | SQL AST path | +| 19 | DESCRIBE PIPE Enterprise | NATIVE | `fixtures/dremio/reference-commands/19_describe_pipe.sql` | `Pipe` command path | +| 20 | DROP | AST | `fixtures/dremio/reference-commands/20_drop.sql` | SQL AST path | +| 21 | DROP BRANCH | NATIVE | `fixtures/dremio/reference-commands/21_drop_branch.sql` | `BranchTag` command path | +| 22 | DROP PIPE Enterprise | NATIVE | `fixtures/dremio/reference-commands/22_drop_pipe.sql` | `Pipe` command path | +| 23 | DROP TAG | NATIVE | `fixtures/dremio/reference-commands/23_drop_tag.sql` | `BranchTag` command path | +| 24 | DROP VIEW | AST | `fixtures/dremio/reference-commands/24_drop_view.sql` | SQL AST path | +| 25 | GRANT/REVOKE Enterprise | NATIVE | `fixtures/dremio/reference-commands/25_grant_revoke.sql` | Generic Dremio command path | +| 26 | INSERT | AST | `fixtures/dremio/reference-commands/26_insert.sql` | SQL AST + insert formatting path | +| 27 | MERGE | AST | `fixtures/dremio/reference-commands/27_merge.sql` | SQL AST path | +| 28 | MERGE BRANCH | NATIVE | `fixtures/dremio/reference-commands/28_merge_branch.sql` | `BranchTag` command path | +| 29 | OPTIMIZE TABLE | NATIVE | `fixtures/dremio/reference-commands/29_optimize_table.sql` | `TableMaintenance` command path | +| 30 | Reflections | NATIVE | `fixtures/dremio/reference-commands/30_reflections.sql` | `Reflection` / `Acceleration*` command paths | +| 31 | RESET QUEUE | NATIVE | `fixtures/dremio/reference-commands/31_reset_queue.sql` | `QueueTag` command path | +| 32 | RESET TAG | NATIVE | `fixtures/dremio/reference-commands/32_reset_tag.sql` | `QueueTag` command path | +| 33 | Role Enterprise | NATIVE | `fixtures/dremio/reference-commands/33_role_enterprise.sql` | Generic Dremio role command path | +| 34 | ROLLBACK | NATIVE | `fixtures/dremio/reference-commands/34_rollback.sql` | `TableMaintenance` command path | +| 35 | Row-Access & Column-Masking | NATIVE | `fixtures/dremio/reference-commands/35_row_access_column_masking.sql` | `RowColumnPolicies` + generic ALTER TABLE path | +| 36 | SELECT | AST | `fixtures/dremio/reference-commands/36_select.sql` | SQL AST path with guarded query formatting | +| 37 | SET QUEUE | NATIVE | `fixtures/dremio/reference-commands/37_set_queue.sql` | `QueueTag` command path | +| 38 | SET TAG | NATIVE | `fixtures/dremio/reference-commands/38_set_tag.sql` | `QueueTag` command path | +| 39 | SHOW BRANCHES | NATIVE | `fixtures/dremio/reference-commands/39_show_branches.sql` | `Show` command path | +| 40 | SHOW CREATE TABLE | AST | `fixtures/dremio/reference-commands/40_show_create_table.sql` | SQL AST path | +| 41 | SHOW CREATE VIEW | AST | `fixtures/dremio/reference-commands/41_show_create_view.sql` | SQL AST path | +| 42 | SHOW LOGS | NATIVE | `fixtures/dremio/reference-commands/42_show_logs.sql` | `Show` command path | +| 43 | SHOW TAGS | NATIVE | `fixtures/dremio/reference-commands/43_show_tags.sql` | `Show` command path | +| 44 | SHOW TBLPROPERTIES | NATIVE | `fixtures/dremio/reference-commands/44_show_tblproperties.sql` | Accepts both short and long property forms | +| 45 | Source SQL Statements | INDEX | `fixtures/dremio/reference-commands/45_source_sql_statements.sql` | Index/overview page | +| 46 | SQL Commands for Apache Iceberg Tables | INDEX | `fixtures/dremio/reference-commands/46_sql_commands_apache_iceberg_tables.sql` | Index/overview page | +| 47 | SQL Commands for Nessie | INDEX | `fixtures/dremio/reference-commands/47_sql_commands_nessie.sql` | Index/overview page | +| 48 | SQL Commands Reference | INDEX | `fixtures/dremio/reference-commands/48_sql_commands_reference.sql` | Index/overview page | +| 49 | Table SQL Statements | INDEX | `fixtures/dremio/reference-commands/49_table_sql_statements.sql` | Index/overview page | +| 50 | TRUNCATE | AST | `fixtures/dremio/reference-commands/50_truncate.sql` | SQL AST path | +| 51 | UPDATE | AST | `fixtures/dremio/reference-commands/51_update.sql` | SQL AST path | +| 52 | USE | NATIVE | `fixtures/dremio/reference-commands/52_use.sql` | `Use` command path | +| 53 | User Enterprise | NATIVE | `fixtures/dremio/reference-commands/53_user_enterprise.sql` | Generic Dremio user command path | +| 54 | User-Defined Functions | NATIVE | `fixtures/dremio/reference-commands/54_user_defined_functions.sql` | Generic Dremio function command path | +| 55 | VACUUM CATALOG | NATIVE | `fixtures/dremio/reference-commands/55_vacuum_catalog.sql` | `VacuumCatalog` command path | +| 56 | VACUUM TABLE | NATIVE | `fixtures/dremio/reference-commands/56_vacuum_table.sql` | `TableMaintenance` command path | +| 57 | WITH | AST | `fixtures/dremio/reference-commands/57_with.sql` | SQL AST path | + +## Snapshot Summary + +- `NATIVE`: 38 +- `AST`: 14 +- `PARTIAL`: 0 +- `UNSUPPORTED`: 0 +- `INDEX`: 5 + +## Evaluation Notes + +- Canonical command corpus: `fixtures/dremio/reference-commands/` with 57 fixture files. +- Parser guard: strict-mode test covers all 57 fixture files. +- Formatter guard: idempotence + keyword-case matrix (upper/lower) covers all 57 fixture files. +- `sqlparser` has been upgraded to `0.61.0` and all parser/formatter tests pass with the upgraded AST APIs. + +## Backlog (Priority Order) + +1. Expand canonical fixtures incrementally from representative statements to broader per-page syntax variants. +2. Keep matrix + fixtures + parser/formatter tests in lockstep for any future Dremio syntax change. diff --git a/docs/dremio.md b/docs/dremio.md index 02c193c..9a6c7a0 100644 --- a/docs/dremio.md +++ b/docs/dremio.md @@ -41,3 +41,8 @@ When Dremio formatting behavior changes: - update `fixtures/dremio/in/` inputs as needed - regenerate `fixtures/dremio/out/` using `--dialect dremio` - keep `fixtures/dremio/expected/` in sync + +## Coverage Tracking + +For command-level support status against the exported Dremio SQL reference, see +[`dremio-support-matrix.md`](dremio-support-matrix.md). diff --git a/fixtures/dremio/reference-commands/01_alter_branch.sql b/fixtures/dremio/reference-commands/01_alter_branch.sql new file mode 100644 index 0000000..dc95ac6 --- /dev/null +++ b/fixtures/dremio/reference-commands/01_alter_branch.sql @@ -0,0 +1,2 @@ +ALTER BRANCH my_branch ASSIGN TAG my_tag IN my_source; +ALTER BRANCH my_branch ASSIGN COMMIT "abc123"; diff --git a/fixtures/dremio/reference-commands/02_alter_folder.sql b/fixtures/dremio/reference-commands/02_alter_folder.sql new file mode 100644 index 0000000..cbae125 --- /dev/null +++ b/fixtures/dremio/reference-commands/02_alter_folder.sql @@ -0,0 +1,2 @@ +ALTER FOLDER my_space.my_folder ROUTE REFLECTIONS TO DEFAULT QUEUE; +ALTER FOLDER my_space.my_folder ROUTE REFLECTIONS TO QUEUE "Queue 1"; diff --git a/fixtures/dremio/reference-commands/03_alter_pipe.sql b/fixtures/dremio/reference-commands/03_alter_pipe.sql new file mode 100644 index 0000000..1343dbd --- /dev/null +++ b/fixtures/dremio/reference-commands/03_alter_pipe.sql @@ -0,0 +1,2 @@ +ALTER PIPE my_pipe SET PIPE_EXECUTION_RUNNING = TRUE; +ALTER PIPE my_pipe AS COPY INTO my_space.my_table FROM '@/files' FILE_FORMAT 'csv'; diff --git a/fixtures/dremio/reference-commands/04_alter_source.sql b/fixtures/dremio/reference-commands/04_alter_source.sql new file mode 100644 index 0000000..11d7fcf --- /dev/null +++ b/fixtures/dremio/reference-commands/04_alter_source.sql @@ -0,0 +1,2 @@ +ALTER SOURCE my_source CLEAR PERMISSION CACHE; +ALTER SOURCE my_source REFRESH STATUS; diff --git a/fixtures/dremio/reference-commands/05_alter_space.sql b/fixtures/dremio/reference-commands/05_alter_space.sql new file mode 100644 index 0000000..de1736b --- /dev/null +++ b/fixtures/dremio/reference-commands/05_alter_space.sql @@ -0,0 +1,2 @@ +ALTER SPACE my_space ROUTE REFLECTIONS TO DEFAULT QUEUE; +ALTER SPACE my_space ROUTE REFLECTIONS TO QUEUE my_queue; diff --git a/fixtures/dremio/reference-commands/06_alter_table.sql b/fixtures/dremio/reference-commands/06_alter_table.sql new file mode 100644 index 0000000..50e543a --- /dev/null +++ b/fixtures/dremio/reference-commands/06_alter_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE my_space.my_table ADD COLUMNS (new_col INT); +ALTER TABLE my_space.my_table CREATE RAW REFLECTION my_ref USING DISPLAY (id, new_col); diff --git a/fixtures/dremio/reference-commands/07_alter_tag.sql b/fixtures/dremio/reference-commands/07_alter_tag.sql new file mode 100644 index 0000000..a36700d --- /dev/null +++ b/fixtures/dremio/reference-commands/07_alter_tag.sql @@ -0,0 +1,2 @@ +ALTER TAG my_tag ASSIGN BRANCH my_branch IN my_source; +ALTER TAG my_tag ASSIGN COMMIT "abc123"; diff --git a/fixtures/dremio/reference-commands/08_alter_view.sql b/fixtures/dremio/reference-commands/08_alter_view.sql new file mode 100644 index 0000000..7b252e7 --- /dev/null +++ b/fixtures/dremio/reference-commands/08_alter_view.sql @@ -0,0 +1,2 @@ +ALTER VIEW my_space.my_view REFRESH METADATA FORCE UPDATE; +ALTER VIEW my_space.my_view ROUTE REFLECTIONS TO DEFAULT QUEUE; diff --git a/fixtures/dremio/reference-commands/09_analyze_table.sql b/fixtures/dremio/reference-commands/09_analyze_table.sql new file mode 100644 index 0000000..d77daec --- /dev/null +++ b/fixtures/dremio/reference-commands/09_analyze_table.sql @@ -0,0 +1,2 @@ +ANALYZE TABLE my_space.my_table FOR ALL COLUMNS COMPUTE STATISTICS; +ANALYZE TABLE my_space.my_table FOR COLUMNS (id, amount) DELETE STATISTICS; diff --git a/fixtures/dremio/reference-commands/10_copy_into.sql b/fixtures/dremio/reference-commands/10_copy_into.sql new file mode 100644 index 0000000..62b9e23 --- /dev/null +++ b/fixtures/dremio/reference-commands/10_copy_into.sql @@ -0,0 +1,2 @@ +COPY INTO my_space.my_table FROM '@/files' FILE_FORMAT 'csv'; +COPY INTO TABLE my_space.my_table FROM '@/files' FILE_FORMAT 'json'; diff --git a/fixtures/dremio/reference-commands/11_create_branch.sql b/fixtures/dremio/reference-commands/11_create_branch.sql new file mode 100644 index 0000000..aee4a45 --- /dev/null +++ b/fixtures/dremio/reference-commands/11_create_branch.sql @@ -0,0 +1,2 @@ +CREATE BRANCH IF NOT EXISTS my_branch IN my_source; +CREATE BRANCH my_branch AT TAG my_tag IN my_source; diff --git a/fixtures/dremio/reference-commands/12_create_folder.sql b/fixtures/dremio/reference-commands/12_create_folder.sql new file mode 100644 index 0000000..79ccba1 --- /dev/null +++ b/fixtures/dremio/reference-commands/12_create_folder.sql @@ -0,0 +1,2 @@ +CREATE FOLDER IF NOT EXISTS my_source.my_folder; +CREATE FOLDER my_source.my_folder2 AT BRANCH my_branch; diff --git a/fixtures/dremio/reference-commands/13_create_pipe.sql b/fixtures/dremio/reference-commands/13_create_pipe.sql new file mode 100644 index 0000000..89fe526 --- /dev/null +++ b/fixtures/dremio/reference-commands/13_create_pipe.sql @@ -0,0 +1,2 @@ +CREATE PIPE IF NOT EXISTS my_pipe NOTIFICATION_PROVIDER AWS_SQS NOTIFICATION_QUEUE_REFERENCE 'arn:aws:sqs:::queue' AS COPY INTO my_space.my_table FROM '@/files' FILE_FORMAT 'csv'; +CREATE PIPE my_pipe2 AS COPY INTO my_space.my_table FROM '@/files' FILE_FORMAT 'parquet'; diff --git a/fixtures/dremio/reference-commands/14_create_table.sql b/fixtures/dremio/reference-commands/14_create_table.sql new file mode 100644 index 0000000..9cd482e --- /dev/null +++ b/fixtures/dremio/reference-commands/14_create_table.sql @@ -0,0 +1,2 @@ +CREATE TABLE IF NOT EXISTS my_space.my_table (id INT, created_at TIMESTAMP); +CREATE TABLE my_space.partitioned (id INT, event_ts TIMESTAMP) PARTITION BY (MONTH(event_ts)); diff --git a/fixtures/dremio/reference-commands/15_create_table_as.sql b/fixtures/dremio/reference-commands/15_create_table_as.sql new file mode 100644 index 0000000..9a2fa42 --- /dev/null +++ b/fixtures/dremio/reference-commands/15_create_table_as.sql @@ -0,0 +1,2 @@ +CREATE TABLE my_space.ctas_table AS SELECT * FROM my_space.my_table; +CREATE TABLE my_space.ctas_partitioned PARTITION BY (MONTH(created_at)) AS SELECT id, created_at FROM my_space.my_table; diff --git a/fixtures/dremio/reference-commands/16_create_tag.sql b/fixtures/dremio/reference-commands/16_create_tag.sql new file mode 100644 index 0000000..9be03b0 --- /dev/null +++ b/fixtures/dremio/reference-commands/16_create_tag.sql @@ -0,0 +1,2 @@ +CREATE TAG IF NOT EXISTS my_tag IN my_source; +CREATE TAG my_tag AT BRANCH my_branch IN my_source; diff --git a/fixtures/dremio/reference-commands/17_create_view.sql b/fixtures/dremio/reference-commands/17_create_view.sql new file mode 100644 index 0000000..39fbf29 --- /dev/null +++ b/fixtures/dremio/reference-commands/17_create_view.sql @@ -0,0 +1,2 @@ +CREATE OR REPLACE VIEW my_space.my_view AS SELECT id FROM my_space.my_table; +CREATE VIEW my_space.my_view2 AS SELECT id, SUM(amount) AS total_amount FROM my_space.my_table GROUP BY id; diff --git a/fixtures/dremio/reference-commands/18_delete.sql b/fixtures/dremio/reference-commands/18_delete.sql new file mode 100644 index 0000000..8eb4648 --- /dev/null +++ b/fixtures/dremio/reference-commands/18_delete.sql @@ -0,0 +1,2 @@ +DELETE FROM my_space.my_table WHERE id = 1; +DELETE FROM my_space.my_table WHERE id IN (SELECT id FROM my_space.ids_to_delete); diff --git a/fixtures/dremio/reference-commands/19_describe_pipe.sql b/fixtures/dremio/reference-commands/19_describe_pipe.sql new file mode 100644 index 0000000..0953e76 --- /dev/null +++ b/fixtures/dremio/reference-commands/19_describe_pipe.sql @@ -0,0 +1,2 @@ +DESCRIBE PIPE my_pipe; +DESCRIBE PIPE my_pipe2; diff --git a/fixtures/dremio/reference-commands/20_drop.sql b/fixtures/dremio/reference-commands/20_drop.sql new file mode 100644 index 0000000..a0695e3 --- /dev/null +++ b/fixtures/dremio/reference-commands/20_drop.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS my_space.old_table; +DROP VIEW IF EXISTS my_space.old_view; diff --git a/fixtures/dremio/reference-commands/21_drop_branch.sql b/fixtures/dremio/reference-commands/21_drop_branch.sql new file mode 100644 index 0000000..95ec68f --- /dev/null +++ b/fixtures/dremio/reference-commands/21_drop_branch.sql @@ -0,0 +1,2 @@ +DROP BRANCH IF EXISTS my_branch IN my_source; +DROP BRANCH my_branch FORCE IN my_source; diff --git a/fixtures/dremio/reference-commands/22_drop_pipe.sql b/fixtures/dremio/reference-commands/22_drop_pipe.sql new file mode 100644 index 0000000..005c3f1 --- /dev/null +++ b/fixtures/dremio/reference-commands/22_drop_pipe.sql @@ -0,0 +1,2 @@ +DROP PIPE my_pipe; +DROP PIPE my_pipe2; diff --git a/fixtures/dremio/reference-commands/23_drop_tag.sql b/fixtures/dremio/reference-commands/23_drop_tag.sql new file mode 100644 index 0000000..2ceeb0c --- /dev/null +++ b/fixtures/dremio/reference-commands/23_drop_tag.sql @@ -0,0 +1,2 @@ +DROP TAG IF EXISTS my_tag IN my_source; +DROP TAG my_tag FORCE IN my_source; diff --git a/fixtures/dremio/reference-commands/24_drop_view.sql b/fixtures/dremio/reference-commands/24_drop_view.sql new file mode 100644 index 0000000..b23a176 --- /dev/null +++ b/fixtures/dremio/reference-commands/24_drop_view.sql @@ -0,0 +1,2 @@ +DROP VIEW IF EXISTS my_space.my_view; +DROP VIEW my_space.my_view2; diff --git a/fixtures/dremio/reference-commands/25_grant_revoke.sql b/fixtures/dremio/reference-commands/25_grant_revoke.sql new file mode 100644 index 0000000..e51ff8d --- /dev/null +++ b/fixtures/dremio/reference-commands/25_grant_revoke.sql @@ -0,0 +1,2 @@ +GRANT SELECT ON TABLE my_space.my_table TO USER analyst_user; +REVOKE SELECT ON TABLE my_space.my_table FROM USER analyst_user; diff --git a/fixtures/dremio/reference-commands/26_insert.sql b/fixtures/dremio/reference-commands/26_insert.sql new file mode 100644 index 0000000..525617f --- /dev/null +++ b/fixtures/dremio/reference-commands/26_insert.sql @@ -0,0 +1,2 @@ +INSERT INTO my_space.my_table (id, amount) VALUES (1, 10.5), (2, 20.0); +INSERT INTO my_space.my_table SELECT id, amount FROM my_space.source_table; diff --git a/fixtures/dremio/reference-commands/27_merge.sql b/fixtures/dremio/reference-commands/27_merge.sql new file mode 100644 index 0000000..e289e89 --- /dev/null +++ b/fixtures/dremio/reference-commands/27_merge.sql @@ -0,0 +1,2 @@ +MERGE INTO my_space.target t USING my_space.source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET amount = s.amount WHEN NOT MATCHED THEN INSERT (id, amount) VALUES (s.id, s.amount); +MERGE INTO my_space.target t USING (SELECT id, amount FROM my_space.source) s ON t.id = s.id WHEN MATCHED THEN UPDATE SET amount = s.amount; diff --git a/fixtures/dremio/reference-commands/28_merge_branch.sql b/fixtures/dremio/reference-commands/28_merge_branch.sql new file mode 100644 index 0000000..221d56d --- /dev/null +++ b/fixtures/dremio/reference-commands/28_merge_branch.sql @@ -0,0 +1,2 @@ +MERGE BRANCH my_feature INTO main IN my_source; +MERGE BRANCH my_feature AT COMMIT "abc123" IN my_source; diff --git a/fixtures/dremio/reference-commands/29_optimize_table.sql b/fixtures/dremio/reference-commands/29_optimize_table.sql new file mode 100644 index 0000000..adf3726 --- /dev/null +++ b/fixtures/dremio/reference-commands/29_optimize_table.sql @@ -0,0 +1,2 @@ +OPTIMIZE TABLE my_space.my_table; +OPTIMIZE TABLE my_space.my_table REWRITE DATA USING BIN_PACK (TARGET_FILE_SIZE_MB = 256, MIN_INPUT_FILES = 5); diff --git a/fixtures/dremio/reference-commands/30_reflections.sql b/fixtures/dremio/reference-commands/30_reflections.sql new file mode 100644 index 0000000..4d73068 --- /dev/null +++ b/fixtures/dremio/reference-commands/30_reflections.sql @@ -0,0 +1,2 @@ +CREATE REFLECTION my_ref USING TABLE my_space.my_table; +REFRESH ACCELERATION my_ref WITH (refresh = 'auto'); diff --git a/fixtures/dremio/reference-commands/31_reset_queue.sql b/fixtures/dremio/reference-commands/31_reset_queue.sql new file mode 100644 index 0000000..575c22a --- /dev/null +++ b/fixtures/dremio/reference-commands/31_reset_queue.sql @@ -0,0 +1,2 @@ +RESET QUEUE; +RESET QUEUE my_queue; diff --git a/fixtures/dremio/reference-commands/32_reset_tag.sql b/fixtures/dremio/reference-commands/32_reset_tag.sql new file mode 100644 index 0000000..6827be5 --- /dev/null +++ b/fixtures/dremio/reference-commands/32_reset_tag.sql @@ -0,0 +1,2 @@ +RESET TAG; +RESET TAG release_tag; diff --git a/fixtures/dremio/reference-commands/33_role_enterprise.sql b/fixtures/dremio/reference-commands/33_role_enterprise.sql new file mode 100644 index 0000000..c923e2d --- /dev/null +++ b/fixtures/dremio/reference-commands/33_role_enterprise.sql @@ -0,0 +1,2 @@ +CREATE ROLE analyst_role; +GRANT ROLE analyst_role TO USER analyst_user; diff --git a/fixtures/dremio/reference-commands/34_rollback.sql b/fixtures/dremio/reference-commands/34_rollback.sql new file mode 100644 index 0000000..335575e --- /dev/null +++ b/fixtures/dremio/reference-commands/34_rollback.sql @@ -0,0 +1,2 @@ +ROLLBACK TABLE my_space.my_table TO SNAPSHOT '123'; +ROLLBACK TABLE my_space.my_table TO TIMESTAMP '2024-01-01 00:00:00'; diff --git a/fixtures/dremio/reference-commands/35_row_access_column_masking.sql b/fixtures/dremio/reference-commands/35_row_access_column_masking.sql new file mode 100644 index 0000000..334a435 --- /dev/null +++ b/fixtures/dremio/reference-commands/35_row_access_column_masking.sql @@ -0,0 +1,2 @@ +ROW COLUMN POLICIES ON my_space.my_table; +ALTER TABLE my_space.my_table MODIFY COLUMN ssn SET MASKING POLICY mask_ssn (ssn); diff --git a/fixtures/dremio/reference-commands/36_select.sql b/fixtures/dremio/reference-commands/36_select.sql new file mode 100644 index 0000000..52a1e6a --- /dev/null +++ b/fixtures/dremio/reference-commands/36_select.sql @@ -0,0 +1,2 @@ +SELECT id, amount FROM my_space.my_table WHERE amount > 0 ORDER BY amount DESC LIMIT 10; +WITH ranked AS (SELECT id, amount, ROW_NUMBER() OVER (PARTITION BY id ORDER BY amount DESC) AS rn FROM my_space.my_table) SELECT id, amount FROM ranked QUALIFY rn = 1; diff --git a/fixtures/dremio/reference-commands/37_set_queue.sql b/fixtures/dremio/reference-commands/37_set_queue.sql new file mode 100644 index 0000000..151991b --- /dev/null +++ b/fixtures/dremio/reference-commands/37_set_queue.sql @@ -0,0 +1,2 @@ +SET QUEUE my_queue; +SET QUEUE "High Priority"; diff --git a/fixtures/dremio/reference-commands/38_set_tag.sql b/fixtures/dremio/reference-commands/38_set_tag.sql new file mode 100644 index 0000000..35e01c0 --- /dev/null +++ b/fixtures/dremio/reference-commands/38_set_tag.sql @@ -0,0 +1,2 @@ +SET TAG release_2026; +SET TAG "release/candidate"; diff --git a/fixtures/dremio/reference-commands/39_show_branches.sql b/fixtures/dremio/reference-commands/39_show_branches.sql new file mode 100644 index 0000000..277dfc2 --- /dev/null +++ b/fixtures/dremio/reference-commands/39_show_branches.sql @@ -0,0 +1,2 @@ +SHOW BRANCHES; +SHOW BRANCHES IN my_source; diff --git a/fixtures/dremio/reference-commands/40_show_create_table.sql b/fixtures/dremio/reference-commands/40_show_create_table.sql new file mode 100644 index 0000000..2c66f59 --- /dev/null +++ b/fixtures/dremio/reference-commands/40_show_create_table.sql @@ -0,0 +1,2 @@ +SHOW CREATE TABLE my_space.my_table; +SHOW CREATE TABLE my_space.my_table AT BRANCH main; diff --git a/fixtures/dremio/reference-commands/41_show_create_view.sql b/fixtures/dremio/reference-commands/41_show_create_view.sql new file mode 100644 index 0000000..32c7fc9 --- /dev/null +++ b/fixtures/dremio/reference-commands/41_show_create_view.sql @@ -0,0 +1,2 @@ +SHOW CREATE VIEW my_space.my_view; +SHOW CREATE VIEW my_space.my_view AT TAG release_2026; diff --git a/fixtures/dremio/reference-commands/42_show_logs.sql b/fixtures/dremio/reference-commands/42_show_logs.sql new file mode 100644 index 0000000..fb3194b --- /dev/null +++ b/fixtures/dremio/reference-commands/42_show_logs.sql @@ -0,0 +1,2 @@ +SHOW LOGS; +SHOW LOGS AT BRANCH main IN my_source; diff --git a/fixtures/dremio/reference-commands/43_show_tags.sql b/fixtures/dremio/reference-commands/43_show_tags.sql new file mode 100644 index 0000000..119a7a1 --- /dev/null +++ b/fixtures/dremio/reference-commands/43_show_tags.sql @@ -0,0 +1,2 @@ +SHOW TAGS; +SHOW TAGS IN my_source; diff --git a/fixtures/dremio/reference-commands/44_show_tblproperties.sql b/fixtures/dremio/reference-commands/44_show_tblproperties.sql new file mode 100644 index 0000000..9fadd53 --- /dev/null +++ b/fixtures/dremio/reference-commands/44_show_tblproperties.sql @@ -0,0 +1,2 @@ +SHOW TBLPROPERTIES my_space.my_table; +SHOW TABLE PROPERTIES my_space.my_table; diff --git a/fixtures/dremio/reference-commands/45_source_sql_statements.sql b/fixtures/dremio/reference-commands/45_source_sql_statements.sql new file mode 100644 index 0000000..8fc0146 --- /dev/null +++ b/fixtures/dremio/reference-commands/45_source_sql_statements.sql @@ -0,0 +1,2 @@ +ALTER SOURCE my_source REFRESH STATUS; +SHOW BRANCHES IN my_source; diff --git a/fixtures/dremio/reference-commands/46_sql_commands_apache_iceberg_tables.sql b/fixtures/dremio/reference-commands/46_sql_commands_apache_iceberg_tables.sql new file mode 100644 index 0000000..a642bc4 --- /dev/null +++ b/fixtures/dremio/reference-commands/46_sql_commands_apache_iceberg_tables.sql @@ -0,0 +1,2 @@ +OPTIMIZE TABLE my_space.iceberg_table; +VACUUM TABLE my_space.iceberg_table EXPIRE SNAPSHOTS RETAIN_LAST = 5; diff --git a/fixtures/dremio/reference-commands/47_sql_commands_nessie.sql b/fixtures/dremio/reference-commands/47_sql_commands_nessie.sql new file mode 100644 index 0000000..f32a129 --- /dev/null +++ b/fixtures/dremio/reference-commands/47_sql_commands_nessie.sql @@ -0,0 +1,2 @@ +CREATE BRANCH dev IN my_source; +VACUUM CATALOG my_source; diff --git a/fixtures/dremio/reference-commands/48_sql_commands_reference.sql b/fixtures/dremio/reference-commands/48_sql_commands_reference.sql new file mode 100644 index 0000000..204f487 --- /dev/null +++ b/fixtures/dremio/reference-commands/48_sql_commands_reference.sql @@ -0,0 +1,2 @@ +USE my_source; +SELECT * FROM my_space.my_table; diff --git a/fixtures/dremio/reference-commands/49_table_sql_statements.sql b/fixtures/dremio/reference-commands/49_table_sql_statements.sql new file mode 100644 index 0000000..fda981e --- /dev/null +++ b/fixtures/dremio/reference-commands/49_table_sql_statements.sql @@ -0,0 +1,2 @@ +CREATE TABLE my_space.new_table (id INT); +TRUNCATE TABLE my_space.new_table; diff --git a/fixtures/dremio/reference-commands/50_truncate.sql b/fixtures/dremio/reference-commands/50_truncate.sql new file mode 100644 index 0000000..ae8e883 --- /dev/null +++ b/fixtures/dremio/reference-commands/50_truncate.sql @@ -0,0 +1,2 @@ +TRUNCATE TABLE my_space.my_table; +TRUNCATE my_space.my_table; diff --git a/fixtures/dremio/reference-commands/51_update.sql b/fixtures/dremio/reference-commands/51_update.sql new file mode 100644 index 0000000..7282b58 --- /dev/null +++ b/fixtures/dremio/reference-commands/51_update.sql @@ -0,0 +1,2 @@ +UPDATE my_space.my_table SET amount = amount + 1 WHERE id = 1; +UPDATE my_space.my_table AS t SET amount = s.amount FROM my_space.source_table AS s WHERE t.id = s.id; diff --git a/fixtures/dremio/reference-commands/52_use.sql b/fixtures/dremio/reference-commands/52_use.sql new file mode 100644 index 0000000..c05cc0e --- /dev/null +++ b/fixtures/dremio/reference-commands/52_use.sql @@ -0,0 +1,2 @@ +USE my_source; +USE BRANCH dev IN my_source; diff --git a/fixtures/dremio/reference-commands/53_user_enterprise.sql b/fixtures/dremio/reference-commands/53_user_enterprise.sql new file mode 100644 index 0000000..aa45141 --- /dev/null +++ b/fixtures/dremio/reference-commands/53_user_enterprise.sql @@ -0,0 +1,2 @@ +CREATE USER analyst_user; +ALTER USER analyst_user SET PASSWORD 'Secret123!'; diff --git a/fixtures/dremio/reference-commands/54_user_defined_functions.sql b/fixtures/dremio/reference-commands/54_user_defined_functions.sql new file mode 100644 index 0000000..3fe937e --- /dev/null +++ b/fixtures/dremio/reference-commands/54_user_defined_functions.sql @@ -0,0 +1,2 @@ +SHOW FUNCTIONS LIKE 'mask_%'; +CREATE FUNCTION hello() RETURNS VARCHAR RETURN SELECT 'hello'; diff --git a/fixtures/dremio/reference-commands/55_vacuum_catalog.sql b/fixtures/dremio/reference-commands/55_vacuum_catalog.sql new file mode 100644 index 0000000..2fd3c72 --- /dev/null +++ b/fixtures/dremio/reference-commands/55_vacuum_catalog.sql @@ -0,0 +1,2 @@ +VACUUM CATALOG my_source; +VACUUM CATALOG my_source EXCLUDE (my_table AT BRANCH dev); diff --git a/fixtures/dremio/reference-commands/56_vacuum_table.sql b/fixtures/dremio/reference-commands/56_vacuum_table.sql new file mode 100644 index 0000000..679c454 --- /dev/null +++ b/fixtures/dremio/reference-commands/56_vacuum_table.sql @@ -0,0 +1,2 @@ +VACUUM TABLE my_space.my_table EXPIRE SNAPSHOTS RETAIN_LAST = 10; +VACUUM TABLE my_space.my_table REMOVE ORPHAN FILES OLDER_THAN = '2024-01-01 00:00:00.000'; diff --git a/fixtures/dremio/reference-commands/57_with.sql b/fixtures/dremio/reference-commands/57_with.sql new file mode 100644 index 0000000..30ca623 --- /dev/null +++ b/fixtures/dremio/reference-commands/57_with.sql @@ -0,0 +1,2 @@ +WITH cte AS (SELECT id FROM my_space.my_table) SELECT * FROM cte; +WITH a AS (SELECT 1 AS id), b AS (SELECT id FROM a) SELECT id FROM b; diff --git a/src/format/sql/case.rs b/src/format/sql/case.rs index d3ae0a2..00b025c 100644 --- a/src/format/sql/case.rs +++ b/src/format/sql/case.rs @@ -1,27 +1,25 @@ -use sqlparser::ast::Expr; +use sqlparser::ast::{CaseWhen, Expr}; use crate::config::FormatterConfig; use crate::format::doc::Doc; pub(super) fn format_case( operand: Option<&Expr>, - conditions: &[Expr], - results: &[Expr], + branches: &[CaseWhen], else_result: &Option>, inline_limit: usize, cfg: &FormatterConfig, alias_tracker: &mut super::RelationAliasTracker, ) -> Doc { - let branch_count = conditions.len(); - let inline_len = estimate_case_inline_length(operand, conditions, results, else_result) - .unwrap_or(usize::MAX); + let branch_count = branches.len(); + let inline_len = + estimate_case_inline_length(operand, branches, else_result).unwrap_or(usize::MAX); let force_multiline = branch_count > 1 || inline_len > inline_limit; if force_multiline { format_case_multiline( operand, - conditions, - results, + branches, else_result, inline_limit, cfg, @@ -30,8 +28,7 @@ pub(super) fn format_case( } else { format_case_inline( operand, - conditions, - results, + branches, else_result, inline_limit, cfg, @@ -42,8 +39,7 @@ pub(super) fn format_case( fn format_case_inline( operand: Option<&Expr>, - conditions: &[Expr], - results: &[Expr], + branches: &[CaseWhen], else_result: &Option>, inline_limit: usize, cfg: &FormatterConfig, @@ -56,15 +52,25 @@ fn format_case_inline( parts.push(super::format_expr(op, inline_limit, cfg, alias_tracker)); } - for (cond, res) in conditions.iter().zip(results.iter()) { + for branch in branches { parts.push(Doc::Space); parts.push(super::keyword_doc(cfg, "WHEN")); parts.push(Doc::Space); - parts.push(super::format_expr(cond, inline_limit, cfg, alias_tracker)); + parts.push(super::format_expr( + &branch.condition, + inline_limit, + cfg, + alias_tracker, + )); parts.push(Doc::Space); parts.push(super::keyword_doc(cfg, "THEN")); parts.push(Doc::Space); - parts.push(super::format_expr(res, inline_limit, cfg, alias_tracker)); + parts.push(super::format_expr( + &branch.result, + inline_limit, + cfg, + alias_tracker, + )); } if let Some(res) = else_result { @@ -82,8 +88,7 @@ fn format_case_inline( fn format_case_multiline( operand: Option<&Expr>, - conditions: &[Expr], - results: &[Expr], + branches: &[CaseWhen], else_result: &Option>, inline_limit: usize, cfg: &FormatterConfig, @@ -96,18 +101,18 @@ fn format_case_multiline( } let mut lines = Vec::new(); - for (idx, (cond, res)) in conditions.iter().zip(results.iter()).enumerate() { + for (idx, branch) in branches.iter().enumerate() { if idx > 0 { lines.push(Doc::Line); } lines.push(Doc::Group(vec![ super::keyword_doc(cfg, "WHEN"), Doc::Space, - super::format_expr(cond, inline_limit, cfg, alias_tracker), + super::format_expr(&branch.condition, inline_limit, cfg, alias_tracker), Doc::Space, super::keyword_doc(cfg, "THEN"), Doc::Space, - super::format_expr(res, inline_limit, cfg, alias_tracker), + super::format_expr(&branch.result, inline_limit, cfg, alias_tracker), ])); } @@ -138,8 +143,7 @@ fn format_case_multiline( fn estimate_case_inline_length( operand: Option<&Expr>, - conditions: &[Expr], - results: &[Expr], + branches: &[CaseWhen], else_result: &Option>, ) -> Option { let mut len = "CASE".len(); @@ -148,10 +152,10 @@ fn estimate_case_inline_length( len = len.checked_add(1 + op.to_string().len())?; } - for (cond, res) in conditions.iter().zip(results.iter()) { + for branch in branches { len = len - .checked_add(1 + "WHEN".len() + 1 + cond.to_string().len())? - .checked_add(1 + "THEN".len() + 1 + res.to_string().len())?; + .checked_add(1 + "WHEN".len() + 1 + branch.condition.to_string().len())? + .checked_add(1 + "THEN".len() + 1 + branch.result.to_string().len())?; } if let Some(res) = else_result { diff --git a/src/format/sql/dremio.rs b/src/format/sql/dremio.rs index 77f5c82..f0fb4d7 100644 --- a/src/format/sql/dremio.rs +++ b/src/format/sql/dremio.rs @@ -73,7 +73,9 @@ pub(super) fn format_dremio_command(cmd: &DremioCommand, cfg: &FormatterConfig) Pipe { verb, rest } => format_pipe(verb, rest, cfg), TableMaintenance { verb, rest } => { if verb.eq_ignore_ascii_case("COPY INTO TABLE") { - format_copy_into(rest, cfg) + format_copy_into(rest, true, cfg) + } else if verb.eq_ignore_ascii_case("COPY INTO") { + format_copy_into(rest, false, cfg) } else { format_optimize_or_vacuum(rest, verb, cfg) } @@ -109,6 +111,10 @@ pub(super) fn format_dremio_command(cmd: &DremioCommand, cfg: &FormatterConfig) format_command_with_rest(&["ROW", "COLUMN", "POLICIES"], rest, cfg, false) } QueueTag { verb, kind, rest } => format_command_with_rest(&[verb, kind], rest, cfg, false), + Generic { head, rest } => { + let parts = head.iter().map(String::as_str).collect::>(); + format_command_with_rest(&parts, rest, cfg, false) + } } } @@ -174,18 +180,20 @@ fn format_alter_pds(rest: &str, cfg: &FormatterConfig) -> Doc { format_command_with_rest(&["ALTER", "PDS"], rest, cfg, false) } -fn format_copy_into(rest: &str, cfg: &FormatterConfig) -> Doc { +fn format_copy_into(rest: &str, with_table_keyword: bool, cfg: &FormatterConfig) -> Doc { // Copy Into can have USING/WITH options; break into target / using / with blocks. let rest = rest.trim(); + let head = if with_table_keyword { + "COPY INTO TABLE" + } else { + "COPY INTO" + }; if rest.is_empty() { - return Doc::Text(apply_keyword_case("COPY INTO TABLE", cfg)); + return Doc::Text(apply_keyword_case(head, cfg)); } let (target, using_block, with_block) = split_copy_into_parts(rest); - let mut parts = vec![ - Doc::Text(apply_keyword_case("COPY INTO TABLE", cfg)), - Doc::Line, - ]; + let mut parts = vec![Doc::Text(apply_keyword_case(head, cfg)), Doc::Line]; parts.push(Doc::Indent(Box::new(Doc::Text(target.to_string())))); if let Some(using) = using_block { diff --git a/src/format/sql/expr.rs b/src/format/sql/expr.rs index b311917..d098dac 100644 --- a/src/format/sql/expr.rs +++ b/src/format/sql/expr.rs @@ -17,6 +17,7 @@ pub(super) fn format_expr( expr, data_type, format, + .. } => super::format_cast_expr( kind, expr, @@ -26,7 +27,7 @@ pub(super) fn format_expr( cfg, alias_tracker, ), - Expr::TypedString { data_type, value } => super::format_typed_string(data_type, value, cfg), + Expr::TypedString(typed) => super::format_typed_string(typed, cfg), Expr::Interval(interval) => { super::format_interval_expr(interval, inline_limit, cfg, alias_tracker) } @@ -98,12 +99,11 @@ pub(super) fn format_expr( Expr::Case { operand, conditions, - results, else_result, + .. } => super::case::format_case( operand.as_deref(), conditions, - results, else_result, inline_limit, cfg, @@ -173,15 +173,16 @@ fn contains_logical_ops(expr: &Expr) -> bool { Expr::Case { operand, conditions, - results, else_result, + .. } => { operand .as_deref() .map(contains_logical_ops) .unwrap_or(false) - || conditions.iter().any(contains_logical_ops) - || results.iter().any(contains_logical_ops) + || conditions.iter().any(|branch| { + contains_logical_ops(&branch.condition) || contains_logical_ops(&branch.result) + }) || else_result .as_deref() .map(contains_logical_ops) diff --git a/src/format/sql/from_join.rs b/src/format/sql/from_join.rs index 9feaa92..db0d8a7 100644 --- a/src/format/sql/from_join.rs +++ b/src/format/sql/from_join.rs @@ -1,4 +1,6 @@ -use sqlparser::ast::{Expr, Function, Join, JoinOperator, TableFactor, TableWithJoins}; +use sqlparser::ast::{ + Expr, Function, Join, JoinOperator, ObjectNamePart, TableFactor, TableWithJoins, +}; use crate::config::FormatterConfig; use crate::format::doc::Doc; @@ -80,6 +82,7 @@ fn format_table_factor( lateral, subquery, alias, + .. } => { let mut parts = Vec::new(); if *lateral { @@ -144,7 +147,7 @@ fn format_function_invocation( ) -> Doc { let mut name = func.name.clone(); if name.0.len() == 1 { - if let Some(ident) = name.0.first_mut() { + if let Some(ObjectNamePart::Identifier(ident)) = name.0.first_mut() { if ident.quote_style.is_none() { ident.value = super::apply_keyword_case(&ident.value, cfg); } @@ -223,10 +226,13 @@ fn table_factor_alias_str(factor: &TableFactor) -> Option { | TableFactor::TableFunction { alias, .. } | TableFactor::UNNEST { alias, .. } | TableFactor::JsonTable { alias, .. } + | TableFactor::OpenJsonTable { alias, .. } | TableFactor::NestedJoin { alias, .. } | TableFactor::Pivot { alias, .. } | TableFactor::Unpivot { alias, .. } - | TableFactor::MatchRecognize { alias, .. } => alias.as_ref().map(|a| a.to_string()), + | TableFactor::MatchRecognize { alias, .. } + | TableFactor::XmlTable { alias, .. } + | TableFactor::SemanticView { alias, .. } => alias.as_ref().map(|a| a.to_string()), } } @@ -236,21 +242,27 @@ fn format_join( alias_tracker: &mut super::RelationAliasTracker, ) -> Doc { let (prefix, constraint, asof_match) = match &join.join_operator { + JoinOperator::Join(constraint) => ("INNER JOIN", Some(constraint), None), JoinOperator::Inner(constraint) => ("INNER JOIN", Some(constraint), None), + JoinOperator::Left(constraint) => ("LEFT JOIN", Some(constraint), None), JoinOperator::LeftOuter(constraint) => ("LEFT JOIN", Some(constraint), None), + JoinOperator::Right(constraint) => ("RIGHT JOIN", Some(constraint), None), JoinOperator::RightOuter(constraint) => ("RIGHT JOIN", Some(constraint), None), JoinOperator::FullOuter(constraint) => ("FULL JOIN", Some(constraint), None), + JoinOperator::Semi(constraint) => ("SEMI JOIN", Some(constraint), None), JoinOperator::LeftSemi(constraint) => ("LEFT SEMI JOIN", Some(constraint), None), JoinOperator::RightSemi(constraint) => ("RIGHT SEMI JOIN", Some(constraint), None), + JoinOperator::Anti(constraint) => ("ANTI JOIN", Some(constraint), None), JoinOperator::LeftAnti(constraint) => ("LEFT ANTI JOIN", Some(constraint), None), JoinOperator::RightAnti(constraint) => ("RIGHT ANTI JOIN", Some(constraint), None), - JoinOperator::CrossJoin => ("CROSS JOIN", None, None), + JoinOperator::CrossJoin(constraint) => ("CROSS JOIN", Some(constraint), None), JoinOperator::CrossApply => ("CROSS APPLY", None, None), JoinOperator::OuterApply => ("OUTER APPLY", None, None), JoinOperator::AsOf { match_condition, constraint, } => ("ASOF JOIN", Some(constraint), Some(match_condition)), + JoinOperator::StraightJoin(constraint) => ("STRAIGHT_JOIN", Some(constraint), None), }; let natural_prefix = matches!(constraint, Some(sqlparser::ast::JoinConstraint::Natural)) diff --git a/src/format/sql/layout.rs b/src/format/sql/layout.rs index b757297..7b742d8 100644 --- a/src/format/sql/layout.rs +++ b/src/format/sql/layout.rs @@ -60,18 +60,24 @@ fn estimate_projection_length(items: &[SelectItem]) -> usize { fn join_prefix_len(join: &Join) -> usize { let (prefix, constraint) = match &join.join_operator { + JoinOperator::Join(constraint) => ("INNER JOIN", Some(constraint)), JoinOperator::Inner(constraint) => ("INNER JOIN", Some(constraint)), + JoinOperator::Left(constraint) => ("LEFT JOIN", Some(constraint)), JoinOperator::LeftOuter(constraint) => ("LEFT JOIN", Some(constraint)), + JoinOperator::Right(constraint) => ("RIGHT JOIN", Some(constraint)), JoinOperator::RightOuter(constraint) => ("RIGHT JOIN", Some(constraint)), JoinOperator::FullOuter(constraint) => ("FULL JOIN", Some(constraint)), + JoinOperator::Semi(constraint) => ("SEMI JOIN", Some(constraint)), JoinOperator::LeftSemi(constraint) => ("LEFT SEMI JOIN", Some(constraint)), JoinOperator::RightSemi(constraint) => ("RIGHT SEMI JOIN", Some(constraint)), + JoinOperator::Anti(constraint) => ("ANTI JOIN", Some(constraint)), JoinOperator::LeftAnti(constraint) => ("LEFT ANTI JOIN", Some(constraint)), JoinOperator::RightAnti(constraint) => ("RIGHT ANTI JOIN", Some(constraint)), - JoinOperator::CrossJoin => ("CROSS JOIN", None), + JoinOperator::CrossJoin(constraint) => ("CROSS JOIN", Some(constraint)), JoinOperator::CrossApply => ("CROSS APPLY", None), JoinOperator::OuterApply => ("OUTER APPLY", None), JoinOperator::AsOf { .. } => ("ASOF JOIN", None), + JoinOperator::StraightJoin(constraint) => ("STRAIGHT_JOIN", Some(constraint)), }; let natural_len = matches!(constraint, Some(sqlparser::ast::JoinConstraint::Natural)) diff --git a/src/format/sql/mod.rs b/src/format/sql/mod.rs index cf745ed..e31feba 100644 --- a/src/format/sql/mod.rs +++ b/src/format/sql/mod.rs @@ -1,9 +1,10 @@ use anyhow::{Context, Result}; use sqlparser::ast::{ ArrayElemTypeDef, CastFormat, CastKind, ColumnDef, CreateTableOptions, DataType, DateTimeField, - Expr, Function, GroupByExpr, HiveDistributionStyle, HiveFormat, Ident, Insert, Interval, - ObjectName, OrderByExpr, Query, Select, SetExpr, Statement, TableConstraint, TableFactor, - TableWithJoins, Value, ViewColumnDef, WindowFrame, WindowSpec, WindowType, + Expr, Function, GroupByExpr, HiveDistributionStyle, HiveFormat, Insert, Interval, ObjectName, + ObjectNamePart, OneOrManyWithParens, OrderByExpr, Query, Select, SetExpr, Statement, + TableConstraint, TableFactor, TableWithJoins, TypedString, Value, ValueWithSpan, ViewColumnDef, + WindowFrame, WindowSpec, WindowType, WrappedCollection, }; use sqlparser::dialect::{AnsiDialect, Dialect, GenericDialect}; @@ -136,143 +137,165 @@ fn format_statement( ) -> Result { match stmt { Statement::Query(query) => format_query(query, cfg, version, alias_tracker), - Statement::CreateTable { - name, - columns, - constraints, - if_not_exists, - or_replace, - temporary, - external, - global, - hive_distribution, - hive_formats, - table_properties, - with_options, - file_format, - location, - query, - like, - clone, - engine, - comment, - default_charset, - collation, - on_commit, - on_cluster, - options, - order_by, - partition_by, - cluster_by, - auto_increment_offset, - strict, - .. - } => { - let simple_layout = query.is_none() - && like.is_none() - && clone.is_none() - && with_options.is_empty() - && table_properties.is_empty() - && file_format.is_none() - && location.is_none() - && engine.is_none() - && comment.is_none() - && default_charset.is_none() - && collation.is_none() - && on_commit.is_none() - && on_cluster.is_none() - && auto_increment_offset.is_none() - && options.as_ref().is_none_or(|v| v.is_empty()) - && order_by.as_ref().is_none_or(|v| v.is_empty()) - && partition_by.is_none() - && cluster_by.as_ref().is_none_or(|v| v.is_empty()) - && matches!(hive_distribution, HiveDistributionStyle::NONE) - && hive_formats.as_ref().is_none_or(hive_format_is_empty) - && !*strict; - - let can_format_ctas = query.is_some() - && like.is_none() - && clone.is_none() - && with_options.is_empty() - && table_properties.is_empty() - && file_format.is_none() - && location.is_none() - && engine.is_none() - && comment.is_none() - && default_charset.is_none() - && collation.is_none() - && on_commit.is_none() - && on_cluster.is_none() - && auto_increment_offset.is_none() - && options.as_ref().is_none_or(|v| v.is_empty()) - && matches!(hive_distribution, HiveDistributionStyle::NONE) - && hive_formats.as_ref().is_none_or(hive_format_is_empty) - && !*strict; + Statement::CreateTable(create_table) => { + let simple_layout = create_table.query.is_none() + && create_table.like.is_none() + && create_table.clone.is_none() + && create_table.version.is_none() + && matches!(create_table.table_options, CreateTableOptions::None) + && create_table.file_format.is_none() + && create_table.location.is_none() + && create_table.comment.is_none() + && create_table.on_commit.is_none() + && create_table.on_cluster.is_none() + && create_table.primary_key.is_none() + && create_table.order_by.is_none() + && create_table.partition_by.is_none() + && create_table.cluster_by.is_none() + && create_table.clustered_by.is_none() + && create_table.inherits.is_none() + && create_table.partition_of.is_none() + && create_table.for_values.is_none() + && matches!(create_table.hive_distribution, HiveDistributionStyle::NONE) + && create_table + .hive_formats + .as_ref() + .is_none_or(hive_format_is_empty) + && !create_table.without_rowid + && !create_table.copy_grants + && !create_table.strict + && !create_table.dynamic + && !create_table.transient + && !create_table.volatile + && !create_table.iceberg + && create_table.enable_schema_evolution.is_none() + && create_table.change_tracking.is_none() + && create_table.data_retention_time_in_days.is_none() + && create_table.max_data_extension_time_in_days.is_none() + && create_table.default_ddl_collation.is_none() + && create_table.with_aggregation_policy.is_none() + && create_table.with_row_access_policy.is_none() + && create_table.with_tags.is_none() + && create_table.external_volume.is_none() + && create_table.base_location.is_none() + && create_table.catalog.is_none() + && create_table.catalog_sync.is_none() + && create_table.storage_serialization_policy.is_none() + && create_table.target_lag.is_none() + && create_table.warehouse.is_none() + && create_table.refresh_mode.is_none() + && create_table.initialize.is_none() + && !create_table.require_user; + + let can_format_ctas = create_table.query.is_some() + && create_table.columns.is_empty() + && create_table.constraints.is_empty() + && create_table.like.is_none() + && create_table.clone.is_none() + && create_table.version.is_none() + && matches!(create_table.table_options, CreateTableOptions::None) + && create_table.file_format.is_none() + && create_table.location.is_none() + && create_table.comment.is_none() + && create_table.on_commit.is_none() + && create_table.on_cluster.is_none() + && create_table.primary_key.is_none() + && create_table.clustered_by.is_none() + && create_table.inherits.is_none() + && create_table.partition_of.is_none() + && create_table.for_values.is_none() + && matches!(create_table.hive_distribution, HiveDistributionStyle::NONE) + && create_table + .hive_formats + .as_ref() + .is_none_or(hive_format_is_empty) + && !create_table.without_rowid + && !create_table.copy_grants + && !create_table.strict + && !create_table.dynamic + && !create_table.transient + && !create_table.volatile + && !create_table.iceberg + && create_table.enable_schema_evolution.is_none() + && create_table.change_tracking.is_none() + && create_table.data_retention_time_in_days.is_none() + && create_table.max_data_extension_time_in_days.is_none() + && create_table.default_ddl_collation.is_none() + && create_table.with_aggregation_policy.is_none() + && create_table.with_row_access_policy.is_none() + && create_table.with_tags.is_none() + && create_table.external_volume.is_none() + && create_table.base_location.is_none() + && create_table.catalog.is_none() + && create_table.catalog_sync.is_none() + && create_table.storage_serialization_policy.is_none() + && create_table.target_lag.is_none() + && create_table.warehouse.is_none() + && create_table.refresh_mode.is_none() + && create_table.initialize.is_none() + && !create_table.require_user; if simple_layout { Ok(format_create_table( - name, - columns, - constraints, + &create_table.name, + &create_table.columns, + &create_table.constraints, TableFormatOptions { - if_not_exists: *if_not_exists, - or_replace: *or_replace, - temporary: *temporary, - external: *external, - global: *global, + if_not_exists: create_table.if_not_exists, + or_replace: create_table.or_replace, + temporary: create_table.temporary, + external: create_table.external, + global: create_table.global, }, cfg, )) } else if can_format_ctas { format_create_table_with_query( - name, - query.as_ref().unwrap(), + &create_table.name, + create_table + .query + .as_ref() + .expect("query exists for ctas") + .as_ref(), TableFormatOptions { - if_not_exists: *if_not_exists, - or_replace: *or_replace, - temporary: *temporary, - external: *external, - global: *global, + if_not_exists: create_table.if_not_exists, + or_replace: create_table.or_replace, + temporary: create_table.temporary, + external: create_table.external, + global: create_table.global, }, cfg, alias_tracker, CreateTableLayout { - order_by: order_by.as_ref(), - partition_by: partition_by.as_deref(), - cluster_by: cluster_by.as_ref(), + order_by: create_table.order_by.as_ref(), + partition_by: create_table.partition_by.as_deref(), + cluster_by: create_table.cluster_by.as_ref(), }, ) } else { Ok(Doc::Text(stringify_with_alias_styles(stmt, alias_tracker))) } } - Statement::CreateView { - name, - columns, - query, - or_replace, - materialized, - options, - cluster_by, - with_no_schema_binding, - if_not_exists, - temporary, - .. - } => { - let simple_layout = matches!(options, CreateTableOptions::None) - && cluster_by.is_empty() - && !*with_no_schema_binding; + Statement::CreateView(create_view) => { + let simple_layout = matches!(create_view.options, CreateTableOptions::None) + && create_view.cluster_by.is_empty() + && !create_view.with_no_schema_binding + && !create_view.or_alter + && !create_view.secure + && create_view.comment.is_none() + && create_view.to.is_none() + && create_view.params.is_none(); if simple_layout { format_create_view( - name, - columns, - query, + &create_view.name, + &create_view.columns, + create_view.query.as_ref(), CreateViewOptions { - or_replace: *or_replace, - materialized: *materialized, - if_not_exists: *if_not_exists, - temporary: *temporary, + or_replace: create_view.or_replace, + materialized: create_view.materialized, + if_not_exists: create_view.if_not_exists, + temporary: create_view.temporary, }, cfg, alias_tracker, @@ -316,9 +339,9 @@ fn format_create_table( } struct CreateTableLayout<'a> { - order_by: Option<&'a Vec>, + order_by: Option<&'a OneOrManyWithParens>, partition_by: Option<&'a Expr>, - cluster_by: Option<&'a Vec>, + cluster_by: Option<&'a WrappedCollection>>, } fn format_create_table_with_query( @@ -350,11 +373,20 @@ fn format_create_table_with_query( } if let Some(items) = layout.cluster_by { - if !items.is_empty() { - let exprs = items.iter().map(|e| e.to_string()).collect(); - parts.push(Doc::Line); - parts.push(format_parenthesized_clause("CLUSTER BY", exprs, cfg, false)); - has_pre_as_clause = true; + match items { + WrappedCollection::Parentheses(exprs) if !exprs.is_empty() => { + let exprs = exprs.iter().map(|e| e.to_string()).collect(); + parts.push(Doc::Line); + parts.push(format_parenthesized_clause("CLUSTER BY", exprs, cfg, false)); + has_pre_as_clause = true; + } + WrappedCollection::NoWrapping(exprs) if !exprs.is_empty() => { + let exprs = exprs.iter().map(|e| e.to_string()).collect(); + parts.push(Doc::Line); + parts.push(format_comma_clause("CLUSTER BY", exprs, cfg)); + has_pre_as_clause = true; + } + _ => {} } } @@ -393,7 +425,7 @@ fn should_prefer_multiline_ctas(query: &Query) -> bool { select.projection.len() > 1 || select.selection.is_some() || select.from.iter().any(|rel| !rel.joins.is_empty()) - || !matches!(&select.group_by, GroupByExpr::Expressions(exprs) if exprs.is_empty()) + || !matches!(&select.group_by, GroupByExpr::Expressions(exprs, _) if exprs.is_empty()) || select.having.is_some() } _ => true, @@ -432,7 +464,7 @@ fn format_insert( head.push(keyword_doc(cfg, "INTO")); } head.push(Doc::Space); - head.push(Doc::Text(insert.table_name.to_string())); + head.push(Doc::Text(insert.table.to_string())); if let Some(alias) = &insert.table_alias { head.push(Doc::Space); @@ -582,7 +614,7 @@ fn format_function_call( let over = func.over.take(); if func.name.0.len() == 1 { - if let Some(ident) = func.name.0.first_mut() { + if let Some(ObjectNamePart::Identifier(ident)) = func.name.0.first_mut() { if ident.quote_style.is_none() && is_builtin_function_name(&ident.value) { ident.value = apply_keyword_case(&ident.value, cfg); } @@ -778,8 +810,8 @@ fn format_window_frame_bound( } } -fn format_value_literal(value: &Value, cfg: &FormatterConfig) -> Doc { - match value { +fn format_value_literal(value: &ValueWithSpan, cfg: &FormatterConfig) -> Doc { + match &value.value { Value::Boolean(flag) => { let text = if *flag { "TRUE" } else { "FALSE" }; Doc::Text(apply_keyword_case(text, cfg)) @@ -801,21 +833,31 @@ fn data_type_has_custom(data_type: &DataType) -> bool { DataType::Custom(_, _) => true, DataType::Array(elem) => match elem { ArrayElemTypeDef::None => false, + ArrayElemTypeDef::Parenthesis(inner) => data_type_has_custom(inner), ArrayElemTypeDef::SquareBracket(inner, _) | ArrayElemTypeDef::AngleBracket(inner) => { data_type_has_custom(inner) } }, - DataType::Struct(fields) => fields + DataType::Struct(fields, _) => fields .iter() .any(|field| data_type_has_custom(&field.field_type)), _ => false, } } -fn format_typed_string(data_type: &DataType, value: &str, cfg: &FormatterConfig) -> Doc { - let type_text = format_data_type(data_type, cfg); - let escaped = value.replace('\'', "''"); - Doc::Text(format!("{type_text} '{escaped}'")) +fn format_typed_string(typed: &TypedString, cfg: &FormatterConfig) -> Doc { + if typed.uses_odbc_syntax { + return Doc::Text(typed.to_string()); + } + + let type_text = format_data_type(&typed.data_type, cfg); + match &typed.value.value { + Value::SingleQuotedString(value) => { + let escaped = value.replace('\'', "''"); + Doc::Text(format!("{type_text} '{escaped}'")) + } + _ => Doc::Text(format!("{type_text} {}", typed.value)), + } } fn format_cast_expr( @@ -1138,11 +1180,13 @@ fn strip_relation_aliases_in_statement( ) { match stmt { Statement::Query(query) => strip_relation_aliases_in_query(text, query, alias_tracker), - Statement::CreateTable { query: Some(q), .. } => { - strip_relation_aliases_in_query(text, q, alias_tracker) + Statement::CreateTable(create_table) => { + if let Some(q) = &create_table.query { + strip_relation_aliases_in_query(text, q, alias_tracker); + } } - Statement::CreateView { query, .. } => { - strip_relation_aliases_in_query(text, query, alias_tracker) + Statement::CreateView(create_view) => { + strip_relation_aliases_in_query(text, create_view.query.as_ref(), alias_tracker) } Statement::Insert(insert) => { if let Some(query) = &insert.source { @@ -1200,6 +1244,7 @@ fn strip_relation_aliases_in_table_factor( | TableFactor::Function { alias, .. } | TableFactor::TableFunction { alias, .. } | TableFactor::JsonTable { alias, .. } + | TableFactor::OpenJsonTable { alias, .. } | TableFactor::UNNEST { alias, .. } => { if let Some(alias) = alias { remove_alias_keyword(text, alias_tracker, alias.to_string()); @@ -1222,6 +1267,11 @@ fn strip_relation_aliases_in_table_factor( remove_alias_keyword(text, alias_tracker, alias.to_string()); } } + TableFactor::XmlTable { alias, .. } | TableFactor::SemanticView { alias, .. } => { + if let Some(alias) = alias { + remove_alias_keyword(text, alias_tracker, alias.to_string()); + } + } } } @@ -1298,11 +1348,24 @@ fn dialect_for_kind(kind: &DialectKind) -> Box { mod tests { use super::*; use crate::config::{DialectKind, KeywordCase, SelectListStyle}; + use std::fs; + use std::path::{Path, PathBuf}; fn format_str(sql: &str, cfg: &FormatterConfig) -> String { format_sql(sql, cfg).expect("format") } + fn reference_command_fixture_paths() -> Vec { + let base = Path::new("fixtures/dremio/reference-commands"); + let mut files = fs::read_dir(base) + .expect("read fixtures/dremio/reference-commands") + .filter_map(|entry| entry.ok().map(|e| e.path())) + .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("sql")) + .collect::>(); + files.sort(); + files + } + #[test] fn cased_builtins_and_booleans() { let cfg = FormatterConfig::default(); @@ -1480,7 +1543,7 @@ WHERE NOT EXISTS ( let out = format_str(sql, &cfg); assert_eq!( out.trim(), - "CREATE TABLE demoCatalog.reporting.\"tables\".orders_partitioned AS\n(\n SELECT\n o.id AS order_id,\n CURRENT_TIMESTAMP AS sync_time,\n o.order_number AS order_number,\n co.site_id AS site_id,\n s.brand_id AS brand_id,\n s.country_id AS site_country_id,\n o.created_at AS created_at,\n COALESCE(dist.revenue_share, 0) AS distribution_amount,\n CASE WHEN o.change_time IS NULL THEN 0 ELSE 1 END AS is_changed,\n CASE WHEN o.cancel_time IS NULL THEN 0 ELSE 1 END AS is_canceled\n FROM demoCatalog.reporting.\"tables\".\"orders\" o\n INNER JOIN demoCatalog.reporting.\"tables\".\"cart_orders\" co\n ON co.order_id = o.id\n INNER JOIN demoCatalog.reporting.\"tables\".\"order_revenues\" orev\n ON orev.order_id = o.id\n INNER JOIN demoCatalog.reporting.\"tables\".\"sites\" s\n ON s.id = co.site_id\n LEFT JOIN (\n SELECT\n i.order_id,\n SUM(i.revenue) AS revenue_share\n FROM demoCatalog.reporting.\"tables\".\"order_revenue_items\" i\n INNER JOIN demoCatalog.reporting.\"tables\".\"product_types\" pt\n ON i.product_type_id = pt.id\n WHERE pt.revenue_group_id = 17\n GROUP BY i.order_id\n ) dist\n ON dist.order_id = o.id\n WHERE NOT EXISTS\n (\n SELECT\n 1\n FROM demoCatalog.reporting.\"tables\".\"order_items\" AS oi\n INNER JOIN demoCatalog.reporting.\"tables\".\"customer_items\" AS ci\n ON ci.id = oi.customer_item_id\n WHERE oi.order_id = o.id\n AND ci.product_type_id = 620\n )\n AND (o.is_test_order = FALSE)\n);" + "CREATE TABLE demoCatalog.reporting.\"tables\".orders_partitioned AS\n(\n SELECT\n o.id AS order_id,\n CURRENT_TIMESTAMP AS sync_time,\n o.order_number AS order_number,\n co.site_id AS site_id,\n s.brand_id AS brand_id,\n s.country_id AS site_country_id,\n o.created_at AS created_at,\n COALESCE(dist.revenue_share, 0) AS distribution_amount,\n CASE WHEN o.change_time IS NULL THEN 0 ELSE 1 END AS is_changed,\n CASE WHEN o.cancel_time IS NULL THEN 0 ELSE 1 END AS is_canceled\n FROM demoCatalog.reporting.\"tables\".\"orders\" o\n INNER JOIN demoCatalog.reporting.\"tables\".\"cart_orders\" co\n ON co.order_id = o.id\n INNER JOIN demoCatalog.reporting.\"tables\".\"order_revenues\" orev\n ON orev.order_id = o.id\n INNER JOIN demoCatalog.reporting.\"tables\".\"sites\" s\n ON s.id = co.site_id\n LEFT JOIN (\n SELECT\n i.order_id,\n SUM(i.revenue) AS revenue_share\n FROM demoCatalog.reporting.\"tables\".\"order_revenue_items\" i\n INNER JOIN demoCatalog.reporting.\"tables\".\"product_types\" pt\n ON i.product_type_id = pt.id\n WHERE pt.revenue_group_id = 17\n GROUP BY i.order_id\n ) dist\n ON dist.order_id = o.id\n WHERE NOT EXISTS\n (\n SELECT\n 1\n FROM demoCatalog.reporting.\"tables\".\"order_items\" oi\n INNER JOIN demoCatalog.reporting.\"tables\".\"customer_items\" ci\n ON ci.id = oi.customer_item_id\n WHERE oi.order_id = o.id\n AND ci.product_type_id = 620\n )\n AND (o.is_test_order = FALSE)\n);" ); } @@ -2206,4 +2269,47 @@ FROM external_cluster.app.raw_segments; let out = format_str(sql, &cfg); assert_eq!(out.trim(), sql.trim()); } + + #[test] + fn formats_all_dremio_reference_command_fixtures_idempotently() { + let files = reference_command_fixture_paths(); + assert_eq!(files.len(), 57, "expected 57 reference command fixtures"); + + let cfg = FormatterConfig { + dialect: DialectKind::Dremio, + ..Default::default() + }; + + for path in files { + let sql = fs::read_to_string(&path).expect("read fixture"); + let once = format_sql(&sql, &cfg).unwrap_or_else(|err| { + panic!("format failed for {:?}: {err}", path); + }); + let twice = format_sql(&once, &cfg).unwrap_or_else(|err| { + panic!("reformat failed for {:?}: {err}", path); + }); + assert_eq!(once, twice, "format not idempotent for {:?}", path); + } + } + + #[test] + fn formats_all_dremio_reference_command_fixtures_for_upper_and_lower_keyword_case() { + let files = reference_command_fixture_paths(); + for keyword_case in [KeywordCase::Upper, KeywordCase::Lower] { + let cfg = FormatterConfig { + dialect: DialectKind::Dremio, + keyword_case, + ..Default::default() + }; + for path in &files { + let sql = fs::read_to_string(path).expect("read fixture"); + format_sql(&sql, &cfg).unwrap_or_else(|err| { + panic!( + "format failed for {:?} with keyword_case {:?}: {err}", + path, keyword_case + ) + }); + } + } + } } diff --git a/src/format/sql/query.rs b/src/format/sql/query.rs index 4a2dbd3..16f025b 100644 --- a/src/format/sql/query.rs +++ b/src/format/sql/query.rs @@ -1,5 +1,8 @@ use anyhow::Result; -use sqlparser::ast::{Cte, GroupByExpr, Query, SelectItem, SetExpr, Values, With}; +use sqlparser::ast::{ + Cte, GroupByExpr, LimitClause, OrderByKind, Query, Select, SelectFlavor, SelectItem, SetExpr, + Values, With, +}; use crate::config::FormatterConfig; use crate::format::doc::Doc; @@ -21,6 +24,10 @@ pub(super) fn format_query_with_layout_preference( alias_tracker: &mut super::RelationAliasTracker, prefer_multiline: bool, ) -> Result { + if query_needs_safe_fallback(query) { + return Ok(Doc::Text(query.to_string())); + } + let mut parts = Vec::new(); if let Some(with) = &query.with { @@ -75,17 +82,25 @@ pub(super) fn format_query_with_layout_preference( } match &select.group_by { - GroupByExpr::All => { + GroupByExpr::All(modifiers) => { parts.push(Doc::Line); - parts.push(Doc::Text("GROUP BY ALL".into())); + if modifiers.is_empty() { + parts.push(Doc::Text("GROUP BY ALL".into())); + } else { + parts.push(Doc::Text(select.group_by.to_string())); + } } - GroupByExpr::Expressions(exprs) if !exprs.is_empty() => { - let exprs = exprs.iter().map(|e| e.to_string()).collect::>(); + GroupByExpr::Expressions(exprs, modifiers) if !exprs.is_empty() => { parts.push(Doc::Line); - if exprs.len() > 1 { - parts.push(super::format_comma_clause_per_line("GROUP BY", exprs, cfg)); + if modifiers.is_empty() { + let exprs = exprs.iter().map(|e| e.to_string()).collect::>(); + if exprs.len() > 1 { + parts.push(super::format_comma_clause_per_line("GROUP BY", exprs, cfg)); + } else { + parts.push(super::format_comma_clause("GROUP BY", exprs, cfg)); + } } else { - parts.push(super::format_comma_clause("GROUP BY", exprs, cfg)); + parts.push(Doc::Text(select.group_by.to_string())); } } _ => {} @@ -101,6 +116,66 @@ pub(super) fn format_query_with_layout_preference( )); } + if !select.cluster_by.is_empty() { + let items = select + .cluster_by + .iter() + .map(|e| e.to_string()) + .collect::>(); + parts.push(Doc::Line); + parts.push(super::format_comma_clause("CLUSTER BY", items, cfg)); + } + + if !select.distribute_by.is_empty() { + let items = select + .distribute_by + .iter() + .map(|e| e.to_string()) + .collect::>(); + parts.push(Doc::Line); + parts.push(super::format_comma_clause("DISTRIBUTE BY", items, cfg)); + } + + if !select.sort_by.is_empty() { + let items = select + .sort_by + .iter() + .map(|e| e.to_string()) + .collect::>(); + parts.push(Doc::Line); + parts.push(super::format_comma_clause("SORT BY", items, cfg)); + } + + if select.window_before_qualify { + if !select.named_window.is_empty() { + parts.push(Doc::Line); + parts.push(format_window_clause(&select.named_window, cfg)); + } + if let Some(qualify) = &select.qualify { + parts.push(Doc::Line); + parts.push(super::format_boolean_clause( + "QUALIFY", + qualify, + cfg, + alias_tracker, + )); + } + } else { + if let Some(qualify) = &select.qualify { + parts.push(Doc::Line); + parts.push(super::format_boolean_clause( + "QUALIFY", + qualify, + cfg, + alias_tracker, + )); + } + if !select.named_window.is_empty() { + parts.push(Doc::Line); + parts.push(format_window_clause(&select.named_window, cfg)); + } + } + append_query_tail(&mut parts, query, cfg); Ok(Doc::Group(parts)) } @@ -136,31 +211,84 @@ pub(super) fn format_query_with_layout_preference( } fn append_query_tail(parts: &mut Vec, query: &Query, cfg: &FormatterConfig) { - if !query.order_by.is_empty() { - let order: Vec = query.order_by.iter().map(|o| o.to_string()).collect(); + if let Some(order_by) = &query.order_by { parts.push(Doc::Line); - parts.push(super::format_comma_clause("ORDER BY", order, cfg)); + if order_by.interpolate.is_none() { + match &order_by.kind { + OrderByKind::Expressions(exprs) => { + let order: Vec = exprs.iter().map(|o| o.to_string()).collect(); + parts.push(super::format_comma_clause("ORDER BY", order, cfg)); + } + OrderByKind::All(_) => parts.push(Doc::Text(order_by.to_string())), + } + } else { + parts.push(Doc::Text(order_by.to_string())); + } } - if let Some(limit) = &query.limit { + if let Some(limit_clause) = &query.limit_clause { + match limit_clause { + LimitClause::LimitOffset { + limit, + offset, + limit_by, + } => { + if let Some(limit) = limit { + parts.push(Doc::Line); + parts.push(Doc::Text(format!( + "{} {}", + super::apply_keyword_case("LIMIT", cfg), + limit + ))); + } + + if let Some(offset) = offset { + parts.push(Doc::Line); + let offset_str = offset.to_string(); + let rendered = offset_str + .strip_prefix("OFFSET ") + .map(|rest| format!("{} {rest}", super::apply_keyword_case("OFFSET", cfg))) + .unwrap_or_else(|| { + format!("{} {offset_str}", super::apply_keyword_case("OFFSET", cfg)) + }); + parts.push(Doc::Text(rendered)); + } + + if !limit_by.is_empty() { + let items = limit_by + .iter() + .map(|expr| expr.to_string()) + .collect::>(); + parts.push(Doc::Line); + parts.push(super::format_comma_clause("BY", items, cfg)); + } + } + LimitClause::OffsetCommaLimit { .. } => { + parts.push(Doc::Line); + parts.push(Doc::Text(limit_clause.to_string().trim().to_string())); + } + } + } + + if let Some(fetch) = &query.fetch { + parts.push(Doc::Line); + parts.push(Doc::Text(fetch.to_string())); + } + + if !query.locks.is_empty() { parts.push(Doc::Line); - parts.push(Doc::Text(format!( - "{} {}", - super::apply_keyword_case("LIMIT", cfg), - limit - ))); + let lock_text = query + .locks + .iter() + .map(|lock| lock.to_string()) + .collect::>() + .join(" "); + parts.push(Doc::Text(lock_text)); } - if let Some(offset) = &query.offset { + if let Some(for_clause) = &query.for_clause { parts.push(Doc::Line); - let offset_str = offset.to_string(); - let rendered = offset_str - .strip_prefix("OFFSET ") - .map(|rest| format!("{} {rest}", super::apply_keyword_case("OFFSET", cfg))) - .unwrap_or_else(|| { - format!("{} {offset_str}", super::apply_keyword_case("OFFSET", cfg)) - }); - parts.push(Doc::Text(rendered)); + parts.push(Doc::Text(for_clause.to_string())); } } @@ -257,3 +385,40 @@ fn format_cte( Ok(Doc::Group(parts)) } + +fn format_window_clause( + windows: &[sqlparser::ast::NamedWindowDefinition], + cfg: &FormatterConfig, +) -> Doc { + let items = windows + .iter() + .map(|window| window.to_string()) + .collect::>(); + super::format_comma_clause("WINDOW", items, cfg) +} + +fn query_needs_safe_fallback(query: &Query) -> bool { + if query.settings.is_some() || query.format_clause.is_some() || !query.pipe_operators.is_empty() + { + return true; + } + + match query.body.as_ref() { + SetExpr::Select(select) => select_needs_safe_fallback(select), + SetExpr::Query(inner) => query_needs_safe_fallback(inner), + _ => false, + } +} + +fn select_needs_safe_fallback(select: &Select) -> bool { + select.top.is_some() + || select.optimizer_hint.is_some() + || select.select_modifiers.is_some() + || select.exclude.is_some() + || select.into.is_some() + || !select.lateral_views.is_empty() + || select.prewhere.is_some() + || select.value_table_mode.is_some() + || !select.connect_by.is_empty() + || !matches!(select.flavor, SelectFlavor::Standard) +} diff --git a/src/parser.rs b/src/parser.rs index 3b6ffdb..4b5aae7 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,7 +3,6 @@ use regex::Regex; use sqlparser::ast::{Query, SetExpr, Statement, TableFactor, TableWithJoins, With}; use sqlparser::dialect::{AnsiDialect, Dialect, GenericDialect}; use sqlparser::parser::{Parser, ParserError}; -use sqlparser::tokenizer::{Token, Tokenizer}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum VersionSelector { @@ -81,6 +80,10 @@ pub enum DremioCommand { if_not_exists: bool, path: String, }, + Generic { + head: Vec, + rest: String, + }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -283,8 +286,7 @@ fn parse_ansi( } }; for stmt in stmts.drain(..) { - let relation_alias_has_as = - collect_relation_alias_flags(&stmt, normalized, dialect.as_ref()); + let relation_alias_has_as = collect_relation_alias_flags(&stmt); out.push(ParsedStatement::Sql { stmt: Box::new(stmt), version: None, @@ -334,8 +336,7 @@ fn parse_dremio( } }; if let Some(stmt) = stmts.pop() { - let relation_alias_has_as = - collect_relation_alias_flags(&stmt, normalized, dialect.as_ref()); + let relation_alias_has_as = collect_relation_alias_flags(&stmt); out.push(ParsedStatement::Sql { stmt: Box::new(stmt), version: parsed.version, @@ -347,220 +348,112 @@ fn parse_dremio( Ok(out) } -fn collect_relation_alias_flags(stmt: &Statement, sql: &str, dialect: &dyn Dialect) -> Vec { - let tokens = match Tokenizer::new(dialect, sql).tokenize() { - Ok(tokens) => tokens, - Err(_) => return Vec::new(), - } - .into_iter() - .filter(|t| !matches!(t, Token::Whitespace(_))) - .collect::>(); - +fn collect_relation_alias_flags(stmt: &Statement) -> Vec { let mut flags = Vec::new(); - collect_aliases_from_statement(stmt, &tokens, &mut flags, dialect); + collect_aliases_from_statement(stmt, &mut flags); flags } -fn collect_aliases_from_statement( - stmt: &Statement, - tokens: &[Token], - flags: &mut Vec, - dialect: &dyn Dialect, -) { +fn collect_aliases_from_statement(stmt: &Statement, flags: &mut Vec) { match stmt { - Statement::Query(query) => { - collect_aliases_from_query(query.as_ref(), tokens, flags, dialect) - } - Statement::CreateTable { query: Some(q), .. } => { - collect_aliases_from_query(q, tokens, flags, dialect); + Statement::Query(query) => collect_aliases_from_query(query.as_ref(), flags), + Statement::CreateTable(create_table) => { + if let Some(q) = &create_table.query { + collect_aliases_from_query(q, flags); + } } - Statement::CreateView { query, .. } => { - collect_aliases_from_query(query, tokens, flags, dialect); + Statement::CreateView(create_view) => { + collect_aliases_from_query(create_view.query.as_ref(), flags); } Statement::Insert(insert) => { if let Some(query) = &insert.source { - collect_aliases_from_query(query, tokens, flags, dialect); + collect_aliases_from_query(query, flags); } } _ => {} }; } -fn collect_aliases_from_query( - query: &Query, - tokens: &[Token], - flags: &mut Vec, - dialect: &dyn Dialect, -) { +fn collect_aliases_from_query(query: &Query, flags: &mut Vec) { if let Some(with) = &query.with { - collect_aliases_from_with(with, tokens, flags, dialect); + collect_aliases_from_with(with, flags); } match query.body.as_ref() { - SetExpr::Select(select) => collect_aliases_from_select(select, tokens, flags, dialect), - SetExpr::Query(inner) => collect_aliases_from_query(inner.as_ref(), tokens, flags, dialect), + SetExpr::Select(select) => collect_aliases_from_select(select, flags), + SetExpr::Query(inner) => collect_aliases_from_query(inner.as_ref(), flags), _ => {} } } -fn collect_aliases_from_with( - with: &With, - tokens: &[Token], - flags: &mut Vec, - dialect: &dyn Dialect, -) { +fn collect_aliases_from_with(with: &With, flags: &mut Vec) { for cte in &with.cte_tables { - collect_aliases_from_query(cte.query.as_ref(), tokens, flags, dialect); + collect_aliases_from_query(cte.query.as_ref(), flags); } } -fn collect_aliases_from_select( - select: &sqlparser::ast::Select, - tokens: &[Token], - flags: &mut Vec, - dialect: &dyn Dialect, -) { +fn collect_aliases_from_select(select: &sqlparser::ast::Select, flags: &mut Vec) { for rel in &select.from { - collect_aliases_from_table_factor(&rel.relation, tokens, flags, dialect); + collect_aliases_from_table_factor(&rel.relation, flags); for join in &rel.joins { - collect_aliases_from_table_factor(&join.relation, tokens, flags, dialect); + collect_aliases_from_table_factor(&join.relation, flags); } } } -fn collect_aliases_from_table_with_joins( - rel: &TableWithJoins, - tokens: &[Token], - flags: &mut Vec, - dialect: &dyn Dialect, -) { - collect_aliases_from_table_factor(&rel.relation, tokens, flags, dialect); +fn collect_aliases_from_table_with_joins(rel: &TableWithJoins, flags: &mut Vec) { + collect_aliases_from_table_factor(&rel.relation, flags); for join in &rel.joins { - collect_aliases_from_table_factor(&join.relation, tokens, flags, dialect); + collect_aliases_from_table_factor(&join.relation, flags); } } -fn collect_aliases_from_table_factor( - factor: &TableFactor, - tokens: &[Token], - flags: &mut Vec, - dialect: &dyn Dialect, -) { +fn collect_aliases_from_table_factor(factor: &TableFactor, flags: &mut Vec) { match factor { TableFactor::Derived { subquery, alias, .. } => { - collect_aliases_from_query(subquery, tokens, flags, dialect); - if alias.is_some() { - flags.push(relation_alias_has_as(factor, tokens, dialect)); + collect_aliases_from_query(subquery, flags); + if let Some(alias) = alias { + flags.push(alias.explicit); } } TableFactor::Table { alias, .. } | TableFactor::Function { alias, .. } | TableFactor::TableFunction { alias, .. } + | TableFactor::OpenJsonTable { alias, .. } | TableFactor::JsonTable { alias, .. } => { - if alias.is_some() { - flags.push(relation_alias_has_as(factor, tokens, dialect)); + if let Some(alias) = alias { + flags.push(alias.explicit); } } TableFactor::UNNEST { alias, .. } => { - if alias.is_some() { - flags.push(relation_alias_has_as(factor, tokens, dialect)); + if let Some(alias) = alias { + flags.push(alias.explicit); } } TableFactor::NestedJoin { table_with_joins, alias, } => { - collect_aliases_from_table_with_joins(table_with_joins, tokens, flags, dialect); - if alias.is_some() { - flags.push(relation_alias_has_as(factor, tokens, dialect)); + collect_aliases_from_table_with_joins(table_with_joins, flags); + if let Some(alias) = alias { + flags.push(alias.explicit); } } TableFactor::Pivot { table, alias, .. } | TableFactor::Unpivot { table, alias, .. } | TableFactor::MatchRecognize { table, alias, .. } => { - collect_aliases_from_table_factor(table, tokens, flags, dialect); - if alias.is_some() { - flags.push(relation_alias_has_as(factor, tokens, dialect)); + collect_aliases_from_table_factor(table, flags); + if let Some(alias) = alias { + flags.push(alias.explicit); } } - } -} - -fn relation_alias_has_as(factor: &TableFactor, tokens: &[Token], dialect: &dyn Dialect) -> bool { - let alias = match table_factor_alias_str(factor) { - Some(alias) => alias, - None => return true, - }; - - let with_as = factor.to_string(); - let needle = format!(" AS {alias}"); - let without_as = if let Some(pos) = with_as.find(&needle) { - let mut no_as = with_as.clone(); - no_as.replace_range(pos..pos + needle.len(), &format!(" {alias}")); - no_as - } else { - with_as.clone() - }; - - let with_tokens = tokenize_fragment(&with_as, dialect); - let without_tokens = tokenize_fragment(&without_as, dialect); - - let has_with = tokens_contain_pattern(tokens, &with_tokens); - let has_without = tokens_contain_pattern(tokens, &without_tokens); - - match (has_with, has_without) { - (true, false) => true, - (false, true) => false, - (true, true) => true, - (false, false) => false, - } -} - -fn table_factor_alias_str(factor: &TableFactor) -> Option { - match factor { - TableFactor::Table { alias, .. } - | TableFactor::Derived { alias, .. } - | TableFactor::Function { alias, .. } - | TableFactor::TableFunction { alias, .. } - | TableFactor::UNNEST { alias, .. } - | TableFactor::JsonTable { alias, .. } - | TableFactor::NestedJoin { alias, .. } - | TableFactor::Pivot { alias, .. } - | TableFactor::Unpivot { alias, .. } - | TableFactor::MatchRecognize { alias, .. } => alias.as_ref().map(|a| a.to_string()), - } -} - -fn tokenize_fragment(sql: &str, dialect: &dyn Dialect) -> Vec { - Tokenizer::new(dialect, sql) - .tokenize() - .unwrap_or_default() - .into_iter() - .filter(|t| !matches!(t, Token::Whitespace(_))) - .collect() -} - -fn tokens_contain_pattern(haystack: &[Token], needle: &[Token]) -> bool { - if needle.is_empty() { - return false; - } - - haystack.windows(needle.len()).any(|window| { - window - .iter() - .zip(needle.iter()) - .all(|(a, b)| tokens_eq(a, b)) - }) -} - -fn tokens_eq(a: &Token, b: &Token) -> bool { - match (a, b) { - (Token::Word(wa), Token::Word(wb)) => { - wa.quote_style == wb.quote_style && wa.value.eq_ignore_ascii_case(&wb.value) + TableFactor::XmlTable { alias, .. } | TableFactor::SemanticView { alias, .. } => { + if let Some(alias) = alias { + flags.push(alias.explicit); + } } - _ => a == b, } } @@ -754,7 +647,7 @@ fn parse_dremio_command(raw: &str) -> Option { return Some(DremioCommand::AlterPds { rest }); } - if lower.starts_with("copy into table") { + if starts_with_command_prefix(&lower, "copy into table") { let rest = normalized["copy into table".len()..].trim().to_string(); return Some(DremioCommand::TableMaintenance { verb: "COPY INTO TABLE".to_string(), @@ -762,11 +655,24 @@ fn parse_dremio_command(raw: &str) -> Option { }); } + if starts_with_command_prefix(&lower, "copy into") { + let rest = normalized["copy into".len()..].trim().to_string(); + return Some(DremioCommand::TableMaintenance { + verb: "COPY INTO".to_string(), + rest, + }); + } + if lower.starts_with("analyze table") { let rest = normalized["analyze table".len()..].trim().to_string(); return Some(DremioCommand::AnalyzeTable { rest }); } + if starts_with_command_prefix(&lower, "show tblproperties") { + let rest = normalized["show tblproperties".len()..].trim().to_string(); + return Some(DremioCommand::ShowTableProperties { rest }); + } + if lower.starts_with("show table properties") { let rest = normalized["show table properties".len()..] .trim() @@ -814,7 +720,7 @@ fn parse_dremio_command(raw: &str) -> Option { } for kind in ["roles", "users"] { - if lower.starts_with(kind) { + if starts_with_command_prefix(&lower, kind) { let rest = trimmed[kind.len()..].trim().to_string(); return Some(DremioCommand::RolesUsers { kind: kind.to_string(), @@ -823,15 +729,46 @@ fn parse_dremio_command(raw: &str) -> Option { } } - if lower.starts_with("row column policies") || lower.starts_with("row-column policies") { - let rest = trimmed - .trim_start_matches("row column policies") - .trim_start_matches("row-column policies") - .trim() - .to_string(); + if starts_with_command_prefix(&lower, "row column policies") { + let rest = normalized["row column policies".len()..].trim().to_string(); + return Some(DremioCommand::RowColumnPolicies { rest }); + } + + if starts_with_command_prefix(&lower, "row-column policies") { + let rest = normalized["row-column policies".len()..].trim().to_string(); return Some(DremioCommand::RowColumnPolicies { rest }); } + for (head, prefix) in [ + (&["ALTER", "FOLDER"][..], "alter folder"), + (&["ALTER", "SOURCE"][..], "alter source"), + (&["ALTER", "SPACE"][..], "alter space"), + (&["ALTER", "VIEW"][..], "alter view"), + (&["ALTER", "TABLE"][..], "alter table"), + (&["GRANT"][..], "grant"), + (&["REVOKE"][..], "revoke"), + (&["CREATE", "ROLE"][..], "create role"), + (&["DROP", "ROLE"][..], "drop role"), + (&["GRANT", "ROLE"][..], "grant role"), + (&["REVOKE", "ROLE"][..], "revoke role"), + (&["CREATE", "USER"][..], "create user"), + (&["ALTER", "USER"][..], "alter user"), + (&["DROP", "USER"][..], "drop user"), + (&["SHOW", "FUNCTIONS"][..], "show functions"), + (&["CREATE", "FUNCTION"][..], "create function"), + (&["DROP", "FUNCTION"][..], "drop function"), + (&["DESCRIBE", "FUNCTION"][..], "describe function"), + (&["ALTER", "ROLE"][..], "alter role"), + ] { + if starts_with_command_prefix(&lower, prefix) { + let rest = normalized[prefix.len()..].trim().to_string(); + return Some(DremioCommand::Generic { + head: head.iter().map(|s| (*s).to_string()).collect(), + rest, + }); + } + } + // Check for queue/tag commands (set/reset queue/tag) for (verb, prefix) in [ ("set", "set queue"), @@ -943,10 +880,27 @@ fn collapse_spaces(input: &str) -> String { result } +fn starts_with_command_prefix(input_lower: &str, prefix: &str) -> bool { + input_lower == prefix || input_lower.starts_with(&format!("{prefix} ")) +} + #[cfg(test)] mod tests { use super::*; use sqlparser::ast::{SetExpr, Statement}; + use std::fs; + use std::path::{Path, PathBuf}; + + fn reference_command_fixture_paths() -> Vec { + let base = Path::new("fixtures/dremio/reference-commands"); + let mut files = fs::read_dir(base) + .expect("read fixtures/dremio/reference-commands") + .filter_map(|entry| entry.ok().map(|e| e.path())) + .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("sql")) + .collect::>(); + files.sort(); + files + } #[test] fn parses_simple_select() { @@ -1212,7 +1166,11 @@ FROM demoCatalog.reporting."tables"."orders" o "expected relation alias metadata" ); match stmt.as_ref() { - Statement::CreateTable { query: Some(q), .. } => { + Statement::CreateTable(create_table) => { + let q = create_table + .query + .as_ref() + .expect("expected CTAS query to be present"); assert!(matches!( q.body.as_ref(), SetExpr::Select(_) | SetExpr::Query(_) @@ -1224,4 +1182,30 @@ FROM demoCatalog.reporting."tables"."orders" o other => panic!("unexpected parsed statement: {other:?}"), } } + + #[test] + fn parses_copy_into_without_table_keyword() { + let sql = "COPY INTO my_space.my_table FROM '@/files' FILE_FORMAT 'csv'"; + let stmts = parse_sql(sql, DialectKind::Dremio).expect("parse copy into"); + assert_eq!(stmts.len(), 1); + assert!(matches!( + stmts[0], + ParsedStatement::Command { + cmd: DremioCommand::TableMaintenance { .. }, + .. + } + )); + } + + #[test] + fn parses_all_dremio_reference_command_fixtures_in_strict_mode() { + let files = reference_command_fixture_paths(); + assert_eq!(files.len(), 57, "expected 57 reference command fixtures"); + + for path in files { + let sql = fs::read_to_string(&path).expect("read fixture"); + parse_sql_with_options(&sql, DialectKind::Dremio, ParseOptions { strict: true }) + .unwrap_or_else(|err| panic!("strict parse failed for {:?}: {err}", path)); + } + } }