Skip to content

Commit a813f5f

Browse files
authored
feat: format BigQuery, PostgreSQL and T-SQL with SQLQuack (#42)
Pin SQLQuack at main 33e43849, whose SQL Server parser reports where each statement was written and reads procedures, and whose formatter lays out BigQuery and PostgreSQL. - bigquery, postgres and postgresql files are formatted; their corpora match SQLQuack's, with its departures copied. - tsql, mssql and sqlserver files are formatted; format splits a T-SQL file at the statements SQL Server's parser reads, so a batch written without semicolons comes out one statement apart. - Facts ask whether a dialect parses with SELECT 1, not empty SQL, which the Databricks parser refuses; Databricks files were failing format. - The configuration page says which dialects each command reads.
1 parent b504e28 commit a813f5f

14 files changed

Lines changed: 381 additions & 61 deletions

File tree

‎Cargo.lock‎

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

‎Cargo.toml‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ 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 = "edfa30b26a0bab86661f96490b86a4f24b92db0a" }
34-
sqlquack-format = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "edfa30b26a0bab86661f96490b86a4f24b92db0a" }
35-
sqlquack-lint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "edfa30b26a0bab86661f96490b86a4f24b92db0a" }
36-
sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "edfa30b26a0bab86661f96490b86a4f24b92db0a" }
33+
sqlquack-fingerprint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }
34+
sqlquack-format = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }
35+
sqlquack-lint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }
36+
sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }
37+
sqlquack-syntax-sqlserver = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }
3738

3839
[workspace.lints.rust]
3940
unsafe_code = "forbid"

‎crates/sqlclean-cli/skill/references/docs/configuration.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,13 @@ SELECT [order id] FROM orders
123123
error[SQLCLEAN999]: SQL could not be parsed as duckdb: Parse error at line 1, column 17: Expected RBracket, got Var ('id') near [SELECT [order id] FROM orders]
124124
```
125125

126-
SQLClean reads SQL with SQLQuack's parsers. `check` and `fix` read `duckdb`, `generic`, `postgres`,
127-
`snowflake` and `sqlite` files, and `format` lays out `duckdb` and `snowflake` files; a `generic`,
128-
`postgres` or `sqlite` file is checked and fixed but left as written by `format`. A file in any
129-
other dialect, such as `mysql` or `tsql`, is not checked, fixed or formatted: `check` reports it once
130-
as a warning (in JSON, under `unavailable`), `format` leaves it unchanged with a warning, and it
131-
never fails a command or counts as a problem.
126+
SQLClean reads SQL with SQLQuack's parsers. `check` and `fix` read `bigquery`, `databricks`,
127+
`duckdb`, `generic`, `postgres`, `snowflake` and `sqlite` files, and `format` lays out `bigquery`,
128+
`duckdb`, `postgres`, `snowflake` and `tsql` files. A `databricks`, `generic` or `sqlite` file is
129+
checked and fixed but left as written by `format`; a `tsql` file is formatted, by `fix` too, but its
130+
Rules are not checked yet. A file in any other dialect, such as `mysql`, is not checked, fixed or
131+
formatted: `check` reports it once as a warning (in JSON, under `unavailable`), `format` leaves it
132+
unchanged with a warning, and it never fails a command or counts as a problem.
132133

133134
```text
134135
warning: SQL lint is unavailable for the mysql dialect: SQLQuack does not parse it yet

‎crates/sqlclean/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ sqlquack-fingerprint.workspace = true
1818
sqlquack-format.workspace = true
1919
sqlquack-lint.workspace = true
2020
sqlquack-syntax.workspace = true
21+
sqlquack-syntax-sqlserver.workspace = true
2122
rayon.workspace = true
2223
rustc-hash.workspace = true
2324
serde.workspace = true

‎crates/sqlclean/src/sql_facts/_helpers/collection.rs‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ use crate::sql_facts::_helpers::tokens::{
1010
function_nodes, join_clause_span, literal_nodes, star_nodes,
1111
};
1212
use crate::sql_facts::constants::{
13-
ALTER_OBJECT_KINDS, CREATE_OBJECT_KINDS, DATE_PART_CALLS, DATE_PART_NAMES, DROP_OBJECT_KINDS,
14-
UTILITY_KINDS,
13+
ALTER_OBJECT_KINDS, AVAILABILITY_PROBE, CREATE_OBJECT_KINDS, DATE_PART_CALLS, DATE_PART_NAMES,
14+
DROP_OBJECT_KINDS, UTILITY_KINDS,
1515
};
1616
use crate::sql_facts::models::{
1717
ColumnNode, FileFacts, JoinNode, NamedNode, StatementFacts, TableNode,
1818
};
19-
use crate::sql_lint::constants::LINT_API_VERSION;
19+
use crate::sql_lint::constants::{LINT_API_VERSION, TSQL_DIALECTS};
2020
use crate::sql_lint::main::lint::lint;
2121
use crate::sql_lint::main::parse_sql::parse_sql;
2222
use crate::sql_lint::types::{LintRequest, LintResponse, ParsedSql};
@@ -60,6 +60,9 @@ pub(crate) fn file_facts(sql: &str, dialect: &str) -> Result<FileFacts, String>
6060
}
6161

6262
pub(crate) fn statement_ends(sql: &str, dialect: &str) -> Result<Option<Vec<usize>>, String> {
63+
if TSQL_DIALECTS.contains(&dialect.to_ascii_lowercase().as_str()) {
64+
return Ok(Some(sqlserver_statement_ends(sql)));
65+
}
6366
if unavailable(dialect)?.is_some() {
6467
return Ok(None);
6568
}
@@ -69,10 +72,27 @@ pub(crate) fn statement_ends(sql: &str, dialect: &str) -> Result<Option<Vec<usiz
6972
))
7073
}
7174

72-
/// Why facts are unavailable for `dialect`: lint's own answer for a dialect SQLQuack does not parse yet.
75+
/// Where each statement of the batch ends, in characters, as SQL Server's own parser reads it; the whole text when the parser refuses it, so formatting reports why.
76+
fn sqlserver_statement_ends(sql: &str) -> Vec<usize> {
77+
let ranges = sqlquack_syntax_sqlserver::parse_strict(sql)
78+
.map(|parsed| parsed.ranges)
79+
.unwrap_or_default();
80+
let ends: Vec<usize> = ranges
81+
.iter()
82+
.filter(|range| range.depth == 0)
83+
.map(|range| sql[..range.end].chars().count())
84+
.collect();
85+
match ends.is_empty() {
86+
true => vec![sql.chars().count()],
87+
false => ends,
88+
}
89+
}
90+
91+
/// Why facts are unavailable for `dialect`: lint's own answer for a dialect SQLQuack does not parse yet, asked with a statement every parsed dialect reads, since some refuse empty input.
7392
fn unavailable(dialect: &str) -> Result<Option<String>, String> {
7493
let probe: LintResponse = lint(LintRequest {
7594
version: LINT_API_VERSION,
95+
sql: AVAILABILITY_PROBE.to_owned(),
7696
dialect: dialect.to_owned(),
7797
enabled_rules: Some(Vec::new()),
7898
..LintRequest::default()

‎crates/sqlclean/src/sql_facts/constants.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,5 @@ pub(crate) const UTILITY_KINDS: [(&str, &str); 11] = [
132132
("REFRESH", "refresh"),
133133
("SHOW", "show"),
134134
];
135+
/// The SQL linted to ask whether SQLQuack parses a dialect; some parsers refuse empty input.
136+
pub(crate) const AVAILABILITY_PROBE: &str = "SELECT 1";

‎crates/sqlclean/src/sql_facts/tests/test_file_facts.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,11 @@ fn given_a_dialect_sqlquack_does_not_parse_when_collecting_facts_then_they_are_u
361361
dialect: "duckdb",
362362
expected_unavailable: None,
363363
},
364+
UnavailableFactsTestCase {
365+
description: "Databricks parses, though its parser refuses empty input, so its facts are available",
366+
dialect: "databricks",
367+
expected_unavailable: None,
368+
},
364369
];
365370
for test_case in &test_cases {
366371
let facts: FileFacts = file_facts("SELECT 1; SELECT 2", test_case.dialect)?;

‎crates/sqlclean/src/sql_lint/_helpers/formatter.rs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use sqlquack_syntax::Dialect as SqlQuackDialect;
55
use crate::constants::INTERPOLATION_PLACEHOLDER_PREFIX;
66
use crate::sql_lint::_helpers::cte_macros::take_out_cte_macros;
77
use crate::sql_lint::constants::{
8-
DUCKDB_DIALECT, LINT_API_VERSION, SNOWFLAKE_DIALECT, TSQL_DIALECT,
8+
BIGQUERY_DIALECT, DUCKDB_DIALECT, LINT_API_VERSION, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT,
9+
TSQL_DIALECTS,
910
};
1011
use crate::sql_lint::models::{FormatRequest, FormatResponse};
1112

@@ -62,7 +63,11 @@ fn sqlquack_dialect(dialect: &str) -> Option<(FormatDialect, Option<SqlQuackDial
6263
match dialect.to_ascii_lowercase().as_str() {
6364
SNOWFLAKE_DIALECT => Some((FormatDialect::Snowflake, Some(SqlQuackDialect::Snowflake))),
6465
DUCKDB_DIALECT => Some((FormatDialect::DuckDB, Some(SqlQuackDialect::DuckDB))),
65-
TSQL_DIALECT => Some((FormatDialect::SqlServer, None)),
66+
BIGQUERY_DIALECT => Some((FormatDialect::BigQuery, None)),
67+
name if POSTGRES_DIALECTS.contains(&name) => {
68+
Some((FormatDialect::PostgreSQL, Some(SqlQuackDialect::PostgreSQL)))
69+
}
70+
name if TSQL_DIALECTS.contains(&name) => Some((FormatDialect::SqlServer, None)),
6671
_ => None,
6772
}
6873
}

‎crates/sqlclean/src/sql_lint/constants.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@ pub const LINT_API_VERSION: u32 = 1;
33
pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake";
44
/// The dialect name SQLQuack lays out as DuckDB.
55
pub(crate) const DUCKDB_DIALECT: &str = "duckdb";
6-
/// The dialect name SQLQuack lays out as SQL Server.
7-
pub(crate) const TSQL_DIALECT: &str = "tsql";
6+
/// The dialect name SQLQuack lays out as BigQuery.
7+
pub(crate) const BIGQUERY_DIALECT: &str = "bigquery";
8+
/// The dialect names SQLQuack lays out as PostgreSQL.
9+
pub(crate) const POSTGRES_DIALECTS: [&str; 2] = ["postgres", "postgresql"];
10+
/// The dialect names SQLQuack lays out as SQL Server.
11+
pub(crate) const TSQL_DIALECTS: [&str; 3] = ["tsql", "mssql", "sqlserver"];

0 commit comments

Comments
 (0)