Skip to content

Commit b781945

Browse files
authored
feat(format): lay out SQL Server table hints, table samples, table variables, FOR XML, FOR JSON, FOR SYSTEM_TIME and join hints (#59)
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.
1 parent d9f7b5e commit b781945

4 files changed

Lines changed: 22 additions & 134 deletions

File tree

‎Cargo.lock‎

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ sha2 = "0.10"
3030
similar = "2"
3131
sqlclean = { path = "crates/sqlclean" }
3232
# SQLQuack's formatter and lint engine, and the tokens and trees they read, which replace Polyglot.
33-
sqlquack-fingerprint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlite", "sqlserver"] }
34-
sqlquack-format = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlserver"] }
35-
sqlquack-lint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlite"] }
36-
sqlquack-parse = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" }
37-
sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" }
38-
sqlquack-syntax-sqlserver = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" }
33+
sqlquack-fingerprint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlite", "sqlserver"] }
34+
sqlquack-format = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlserver"] }
35+
sqlquack-lint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlite"] }
36+
sqlquack-parse = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da" }
37+
sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da" }
38+
sqlquack-syntax-sqlserver = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "56c7c15ecafe61ea6650159469234db670dea8da" }
3939

4040
[workspace.lints.rust]
4141
unsafe_code = "forbid"

‎crates/sqlclean/src/sql_lint/tests/fixtures/sqlquack_departures.json‎

Lines changed: 3 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -223,90 +223,13 @@
223223
"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",
224224
"receipt": null
225225
},
226-
{
227-
"dialect": "tsql",
228-
"sql": "SELECT * FROM t TABLESAMPLE (10 PERCENT)",
229-
"formatted": null,
230-
"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",
231-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
232-
},
233-
{
234-
"dialect": "tsql",
235-
"sql": "SELECT * FROM t TABLESAMPLE (20 ROWS)",
236-
"formatted": null,
237-
"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",
238-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
239-
},
240-
{
241-
"dialect": "tsql",
242-
"sql": "SELECT * FROM t TABLESAMPLE (10 PERCENT) REPEATABLE (123)",
243-
"formatted": null,
244-
"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",
245-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
246-
},
247226
{
248227
"dialect": "tsql",
249228
"sql": "SELECT * FROM t WHERE NOT c",
250229
"formatted": null,
251230
"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",
252231
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
253232
},
254-
{
255-
"dialect": "tsql",
256-
"sql": "SELECT * FROM t WITH (TABLOCK, INDEX(myindex))",
257-
"formatted": null,
258-
"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",
259-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
260-
},
261-
{
262-
"dialect": "tsql",
263-
"sql": "SELECT Employee_ID, Department_ID FROM @MyTableVar",
264-
"formatted": null,
265-
"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",
266-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
267-
},
268-
{
269-
"dialect": "tsql",
270-
"sql": "SELECT x FROM @MyTableVar AS m JOIN Employee ON m.EmployeeID = Employee.EmployeeID",
271-
"formatted": null,
272-
"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",
273-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
274-
},
275-
{
276-
"dialect": "tsql",
277-
"sql": "SELECT * FROM t FOR XML PATH, BINARY BASE64, ELEMENTS XSINIL",
278-
"formatted": null,
279-
"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",
280-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
281-
},
282-
{
283-
"dialect": "tsql",
284-
"sql": "SELECT (SELECT TOP 5 a, b FROM t ORDER BY b DESC FOR JSON PATH) AS j FROM x",
285-
"formatted": null,
286-
"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",
287-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
288-
},
289-
{
290-
"dialect": "tsql",
291-
"sql": "SELECT j FROM (SELECT a AS j FROM t FOR JSON PATH) AS x",
292-
"formatted": null,
293-
"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",
294-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
295-
},
296-
{
297-
"dialect": "tsql",
298-
"sql": "SELECT * FROM t FOR JSON PATH, ROOT('Root'), INCLUDE_NULL_VALUES",
299-
"formatted": null,
300-
"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",
301-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
302-
},
303-
{
304-
"dialect": "tsql",
305-
"sql": "SELECT * FROM t FOR BROWSE",
306-
"formatted": null,
307-
"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",
308-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
309-
},
310233
{
311234
"dialect": "tsql",
312235
"sql": "SELECT TOP 10 PERCENT",
@@ -321,53 +244,25 @@
321244
"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",
322245
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
323246
},
324-
{
325-
"dialect": "tsql",
326-
"sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME AS OF 'foo'",
327-
"formatted": null,
328-
"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",
329-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
330-
},
331-
{
332-
"dialect": "tsql",
333-
"sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME AS OF 'foo' AS alias",
334-
"formatted": null,
335-
"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",
336-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
337-
},
338247
{
339248
"dialect": "tsql",
340249
"sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME FROM c TO d",
341250
"formatted": null,
342-
"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",
251+
"reason": "SQL Server rejects it: `Incorrect syntax near 'c'.` (102); SQLQuack's parser refuses it (`Incorrect syntax near 'c'.`); SQLClean formatted it",
343252
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
344253
},
345254
{
346255
"dialect": "tsql",
347256
"sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME BETWEEN c AND d",
348257
"formatted": null,
349-
"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",
258+
"reason": "SQL Server rejects it: `Incorrect syntax near 'c'.` (102); SQLQuack's parser refuses it (`Incorrect syntax near 'c'.`); SQLClean formatted it",
350259
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
351260
},
352261
{
353262
"dialect": "tsql",
354263
"sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME CONTAINED IN (c, d)",
355264
"formatted": null,
356-
"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",
357-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
358-
},
359-
{
360-
"dialect": "tsql",
361-
"sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME ALL AS alias",
362-
"formatted": null,
363-
"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",
364-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
365-
},
366-
{
367-
"dialect": "tsql",
368-
"sql": "SELECT x FROM a INNER LOOP JOIN b ON b.id = a.id",
369-
"formatted": null,
370-
"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",
265+
"reason": "SQL Server rejects it: `Incorrect syntax near 'c'.` (102); SQLQuack's parser refuses it (`Incorrect syntax near 'c'.`); SQLClean formatted it",
371266
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
372267
},
373268
{
@@ -377,13 +272,6 @@
377272
"reason": "SQL Server rejects it: `Incorrect syntax near the keyword 'schema'.` (156); SQLQuack's parser refuses it (`Incorrect syntax near 'schema'.`); SQLClean formatted it",
378273
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
379274
},
380-
{
381-
"dialect": "tsql",
382-
"sql": "SELECT * FROM @x",
383-
"formatted": null,
384-
"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",
385-
"receipt": "crates/sqlquack-format/src/format/tests/fixtures/sqlserver_receipts.jsonl"
386-
},
387275
{
388276
"dialect": "bigquery",
389277
"sql": "SELECT * FROM x.*",

‎crates/sqlclean/src/sql_lint/tests/test_format_corpus.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ fn given_dialect_corpus_when_formatting_then_output_layout_and_refusals_are_stab
4242
test_types::FormatCorpusTestCase {
4343
description: "T-SQL identity corpus",
4444
dialect: "tsql",
45-
expected_formatted: 52,
46-
expected_refused: 23,
45+
expected_formatted: 68,
46+
expected_refused: 7,
4747
},
4848
];
4949
let corpus: Vec<Value> =

0 commit comments

Comments
 (0)