From 2d0b174da71125af5f2454f96acb10482797aca5 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Tue, 10 Nov 2026 02:30:00 +0000 Subject: [PATCH] feat(format): lay out SQL Server table hints, table samples, table variables, FOR XML, FOR JSON, FOR SYSTEM_TIME and join hints Pin SQLQuack 56c7c15e. Its SQL Server parser now reads TABLESAMPLE, WITH (...) table hints, table variables such as @t, FOR XML, FOR JSON and FOR BROWSE, FOR SYSTEM_TIME AS OF and ALL, and join hints such as INNER LOOP JOIN, so format lays these queries out where it left them as written before. 16 T-SQL corpus queries now come out exactly as the corpus records them. FOR SYSTEM_TIME FROM ... TO, BETWEEN and CONTAINED IN with column bounds are still refused, now with SQL Server's own syntax error. Lowercase LOOP, HASH and NOLOCK hints keep the case they were written in, and the T-SQL statement splitter refuses statements SQL Server rejects. --- Cargo.lock | 22 ++-- Cargo.toml | 12 +- .../tests/fixtures/sqlquack_departures.json | 118 +----------------- .../src/sql_lint/tests/test_format_corpus.rs | 4 +- 4 files changed, 22 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97fe6ed..13f299a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -613,7 +613,7 @@ dependencies = [ [[package]] name = "sqlquack-derive" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "proc-macro2", "quote", @@ -623,7 +623,7 @@ dependencies = [ [[package]] name = "sqlquack-fingerprint" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "sha2", "sqlquack-parse", @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "sqlquack-format" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "serde_json", "sqlquack-parse", @@ -643,7 +643,7 @@ dependencies = [ [[package]] name = "sqlquack-lint" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "rayon", "rustc-hash", @@ -656,7 +656,7 @@ dependencies = [ [[package]] name = "sqlquack-parse" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "sqlquack-syntax", "sqlquack-syntax-bigquery", @@ -669,7 +669,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "serde", "serde_json", @@ -682,7 +682,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-bigquery" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "sqlquack-syntax", "stacker", @@ -691,7 +691,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-databricks" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "sqlquack-syntax", "stacker", @@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-postgres" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "sqlquack-syntax", "stacker", @@ -709,7 +709,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-snowflake" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "serde", "serde_json", @@ -722,7 +722,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-sqlserver" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293#f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=56c7c15ecafe61ea6650159469234db670dea8da#56c7c15ecafe61ea6650159469234db670dea8da" dependencies = [ "sqlquack-syntax", "stacker", diff --git a/Cargo.toml b/Cargo.toml index 5630952..7bb002c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,12 +30,12 @@ sha2 = "0.10" similar = "2" sqlclean = { path = "crates/sqlclean" } # SQLQuack's formatter and lint engine, and the tokens and trees they read, which replace Polyglot. -sqlquack-fingerprint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlite", "sqlserver"] } -sqlquack-format = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlserver"] } -sqlquack-lint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlite"] } -sqlquack-parse = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" } -sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" } -sqlquack-syntax-sqlserver = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" } +sqlquack-fingerprint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlite", "sqlserver"] } +sqlquack-format = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlserver"] } +sqlquack-lint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlite"] } +sqlquack-parse = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da" } +sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da" } +sqlquack-syntax-sqlserver = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da" } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/crates/sqlclean/src/sql_lint/tests/fixtures/sqlquack_departures.json b/crates/sqlclean/src/sql_lint/tests/fixtures/sqlquack_departures.json index 6f8d6ce..666687c 100644 --- a/crates/sqlclean/src/sql_lint/tests/fixtures/sqlquack_departures.json +++ b/crates/sqlclean/src/sql_lint/tests/fixtures/sqlquack_departures.json @@ -223,27 +223,6 @@ "reason": "A set operation's ORDER BY puts its items on the next line, as in every other dialect and SQL Server's own `ORDER BY\\n 1`; SQLClean kept them on the ORDER BY line", "receipt": null }, - { - "dialect": "tsql", - "sql": "SELECT * FROM t TABLESAMPLE (10 PERCENT)", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a TABLESAMPLE clause opaque (gap `tablesample`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT * FROM t TABLESAMPLE (20 ROWS)", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a TABLESAMPLE clause opaque (gap `tablesample`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT * FROM t TABLESAMPLE (10 PERCENT) REPEATABLE (123)", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a TABLESAMPLE clause opaque (gap `tablesample`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, { "dialect": "tsql", "sql": "SELECT * FROM t WHERE NOT c", @@ -251,62 +230,6 @@ "reason": "SQL Server rejects it: `An expression of non-boolean type specified in a context where a condition is expected, near 'c'.` (4145); SQLQuack's parser refuses it (`An expression of non-boolean type specified in a context where a condition is expected.`); SQLClean formatted it", "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" }, - { - "dialect": "tsql", - "sql": "SELECT * FROM t WITH (TABLOCK, INDEX(myindex))", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a table hint list opaque (gap `table_hint`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT Employee_ID, Department_ID FROM @MyTableVar", - "formatted": null, - "reason": "SQL Server rejects it: `Must declare the table variable \"@MyTableVar\".` (1087); SQLQuack's parser keeps a table variable source opaque (gap `table_variable`); SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT x FROM @MyTableVar AS m JOIN Employee ON m.EmployeeID = Employee.EmployeeID", - "formatted": null, - "reason": "SQL Server rejects it: `Must declare the table variable \"@MyTableVar\".` (1087); SQLQuack's parser keeps a table variable source opaque (gap `table_variable`); SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT * FROM t FOR XML PATH, BINARY BASE64, ELEMENTS XSINIL", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a FOR XML, FOR JSON or FOR BROWSE clause opaque (gap `for_xml_json`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT (SELECT TOP 5 a, b FROM t ORDER BY b DESC FOR JSON PATH) AS j FROM x", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a FOR XML, FOR JSON or FOR BROWSE clause opaque (gap `for_xml_json`) on FOR JSON in a scalar subquery, a parser false reject; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT j FROM (SELECT a AS j FROM t FOR JSON PATH) AS x", - "formatted": null, - "reason": "SQL Server rejects it at binding: `Invalid column name 'j'.` (207, then 8155), as a FOR JSON derived table needs a column list; SQLQuack's parser keeps a FOR XML, FOR JSON or FOR BROWSE clause opaque (gap `for_xml_json`), even with one; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT * FROM t FOR JSON PATH, ROOT('Root'), INCLUDE_NULL_VALUES", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a FOR XML, FOR JSON or FOR BROWSE clause opaque (gap `for_xml_json`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT * FROM t FOR BROWSE", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a FOR XML, FOR JSON or FOR BROWSE clause opaque (gap `for_xml_json`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, { "dialect": "tsql", "sql": "SELECT TOP 10 PERCENT", @@ -321,53 +244,25 @@ "reason": "SQL Server rejects it: `Incorrect syntax near 'TIES'.` (102); SQLQuack's parser refuses it (`Incorrect syntax near the end of the input.`); SQLClean formatted it", "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" }, - { - "dialect": "tsql", - "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME AS OF 'foo'", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a FOR SYSTEM_TIME clause opaque (gap `system_time`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME AS OF 'foo' AS alias", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a FOR SYSTEM_TIME clause opaque (gap `system_time`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, { "dialect": "tsql", "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME FROM c TO d", "formatted": null, - "reason": "SQL Server rejects it: `Incorrect syntax near 'c'.` (102); SQLQuack's parser keeps a FOR SYSTEM_TIME clause opaque (gap `system_time`); SQLClean formatted it", + "reason": "SQL Server rejects it: `Incorrect syntax near 'c'.` (102); SQLQuack's parser refuses it (`Incorrect syntax near 'c'.`); SQLClean formatted it", "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" }, { "dialect": "tsql", "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME BETWEEN c AND d", "formatted": null, - "reason": "SQL Server rejects it: `Incorrect syntax near 'c'.` (102); SQLQuack's parser keeps a FOR SYSTEM_TIME clause opaque (gap `system_time`); SQLClean formatted it", + "reason": "SQL Server rejects it: `Incorrect syntax near 'c'.` (102); SQLQuack's parser refuses it (`Incorrect syntax near 'c'.`); SQLClean formatted it", "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" }, { "dialect": "tsql", "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME CONTAINED IN (c, d)", "formatted": null, - "reason": "SQL Server rejects it: `Incorrect syntax near 'c'.` (102); SQLQuack's parser keeps a FOR SYSTEM_TIME clause opaque (gap `system_time`); SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME ALL AS alias", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a FOR SYSTEM_TIME clause opaque (gap `system_time`), so the formatter refuses rather than guess; SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, - { - "dialect": "tsql", - "sql": "SELECT x FROM a INNER LOOP JOIN b ON b.id = a.id", - "formatted": null, - "reason": "SQL Server accepts it; SQLQuack's parser keeps a join hint opaque (gap `join_hint`), so the formatter refuses rather than guess; SQLClean formatted it", + "reason": "SQL Server rejects it: `Incorrect syntax near 'c'.` (102); SQLQuack's parser refuses it (`Incorrect syntax near 'c'.`); SQLClean formatted it", "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" }, { @@ -377,13 +272,6 @@ "reason": "SQL Server rejects it: `Incorrect syntax near the keyword 'schema'.` (156); SQLQuack's parser refuses it (`Incorrect syntax near 'schema'.`); SQLClean formatted it", "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" }, - { - "dialect": "tsql", - "sql": "SELECT * FROM @x", - "formatted": null, - "reason": "SQL Server rejects it: `Must declare the table variable \"@x\".` (1087); SQLQuack's parser keeps a table variable source opaque (gap `table_variable`); SQLClean formatted it", - "receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl" - }, { "dialect": "bigquery", "sql": "SELECT * FROM x.*", diff --git a/crates/sqlclean/src/sql_lint/tests/test_format_corpus.rs b/crates/sqlclean/src/sql_lint/tests/test_format_corpus.rs index b62cc7f..c5b21d3 100644 --- a/crates/sqlclean/src/sql_lint/tests/test_format_corpus.rs +++ b/crates/sqlclean/src/sql_lint/tests/test_format_corpus.rs @@ -42,8 +42,8 @@ fn given_dialect_corpus_when_formatting_then_output_layout_and_refusals_are_stab test_types::FormatCorpusTestCase { description: "T-SQL identity corpus", dialect: "tsql", - expected_formatted: 52, - expected_refused: 23, + expected_formatted: 68, + expected_refused: 7, }, ]; let corpus: Vec =