Skip to content

Commit e027a95

Browse files
authored
feat: format Databricks with SQLQuack (#57)
* feat: format Databricks with SQLQuack Databricks files are laid out by SQLQuack's Databricks profile, which reads them with the Databricks scanner and parser. Of the 62 Databricks corpus queries, 60 come out as SQLClean laid them out before. A ${x} variable substitution, which the Databricks parser rejects, is left as written with a warning, and get_json_object is upper-cased as every other call is. * test: expect format to lay out a Databricks file end to end
1 parent e397b73 commit e027a95

11 files changed

Lines changed: 34 additions & 55 deletions

File tree

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ similar = "2"
3131
sqlclean = { path = "crates/sqlclean" }
3232
# SQLQuack's formatter and lint engine, and the tokens and trees they read, which replace Polyglot.
3333
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", "duckdb", "postgres", "snowflake", "sqlserver"] }
34+
sqlquack-format = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlserver"] }
3535
sqlquack-lint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293", features = ["bigquery", "databricks", "duckdb", "postgres", "snowflake", "sqlite"] }
3636
sqlquack-parse = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" }
3737
sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "f53c6a076ad05c5c12047a2dcc9c58e3ddfbe293" }

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ error[SQLCLEAN999]: SQL could not be parsed as duckdb: Parse error at line 1, co
125125

126126
SQLClean reads SQL with SQLQuack's parsers. `check` and `fix` read `bigquery`, `databricks`,
127127
`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
128+
`databricks`, `duckdb`, `postgres`, `snowflake` and `tsql` files. A `generic` or `sqlite` file is
129129
checked and fixed but left as written by `format`; a `tsql` file is formatted, by `fix` too, but its
130130
Rules are not checked yet. A file in any other dialect, such as `mysql`, is not checked, fixed or
131131
formatted: `check` reports it once as a warning (in JSON, under `unavailable`), `format` leaves it

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +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, wrap_query_placeholders};
77
use crate::sql_lint::constants::{
8-
BIGQUERY_DIALECT, DUCKDB_DIALECT, LINT_API_VERSION, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT,
9-
TSQL_DIALECTS,
8+
BIGQUERY_DIALECT, DATABRICKS_DIALECT, DUCKDB_DIALECT, LINT_API_VERSION, POSTGRES_DIALECTS,
9+
SNOWFLAKE_DIALECT, TSQL_DIALECTS,
1010
};
1111
use crate::sql_lint::models::{FormatRequest, FormatResponse};
1212

@@ -64,6 +64,7 @@ fn sqlquack_dialect(dialect: &str) -> Option<(FormatDialect, Option<SqlQuackDial
6464
SNOWFLAKE_DIALECT => Some((FormatDialect::Snowflake, Some(SqlQuackDialect::Snowflake))),
6565
DUCKDB_DIALECT => Some((FormatDialect::DuckDB, Some(SqlQuackDialect::DuckDB))),
6666
BIGQUERY_DIALECT => Some((FormatDialect::BigQuery, None)),
67+
DATABRICKS_DIALECT => Some((FormatDialect::Databricks, None)),
6768
name if POSTGRES_DIALECTS.contains(&name) => {
6869
Some((FormatDialect::PostgreSQL, Some(SqlQuackDialect::PostgreSQL)))
6970
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake";
55
pub(crate) const DUCKDB_DIALECT: &str = "duckdb";
66
/// The dialect name SQLQuack lays out as BigQuery.
77
pub(crate) const BIGQUERY_DIALECT: &str = "bigquery";
8+
/// The dialect name SQLQuack lays out as Databricks.
9+
pub(crate) const DATABRICKS_DIALECT: &str = "databricks";
810
/// The dialect names SQLQuack lays out as PostgreSQL.
911
pub(crate) const POSTGRES_DIALECTS: [&str; 2] = ["postgres", "postgresql"];
1012
/// The dialect names SQLQuack lays out as SQL Server.

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,5 +593,19 @@
593593
"formatted": null,
594594
"reason": "PostgreSQL's scanner reads `<<->>` as one operator, which the shared tokenizer splits",
595595
"receipt": null
596+
},
597+
{
598+
"dialect": "databricks",
599+
"sql": "SELECT ${x} FROM ${y} WHERE ${z} > 1",
600+
"formatted": null,
601+
"reason": "`${x}` is a variable substitution that Databricks replaces before parsing, so its grammar has no such token and the native parser rejects it; SQLClean formatted it",
602+
"receipt": null
603+
},
604+
{
605+
"dialect": "databricks",
606+
"sql": "WITH t AS (SELECT '{\"x-y\": \"z\"}' AS c) SELECT get_json_object(c, '$.x-y') FROM t",
607+
"formatted": "WITH t AS (\n SELECT '{\"x-y\": \"z\"}' AS c\n)\n\nSELECT GET_JSON_OBJECT(c, '$.x-y')\nFROM t",
608+
"reason": "SQLClean kept the call's authored case because its Polyglot oracle rewrites the JSON path `'$.x-y'` as `'$[\"x-y\"]'`, as SQLGlot does; SQLQuack upper-cases a called name in every dialect",
609+
"receipt": null
596610
}
597611
]

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

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,6 @@ fn only_keyword_case_differs(sql: &str, printed: &str, dialect: &str) -> Result<
145145
})
146146
}
147147

148-
/// Check every corpus query of one dialect is left as written and reported unavailable; return how many.
149-
pub(crate) fn check_unavailable_corpus(corpus: &[Value], dialect: &str) -> Result<usize, String> {
150-
let mut count: usize = 0;
151-
for entry in corpus.iter().filter(|entry| entry["dialect"] == dialect) {
152-
let sql = entry["sql"].as_str().unwrap_or_default();
153-
let response = format_once(sql, dialect, Some(CORPUS_LINE_WIDTH))?;
154-
assert_eq!(response["sql"], sql, "{dialect}: {sql}");
155-
assert_eq!(response["formatted"], false, "{dialect}: {sql}");
156-
assert_eq!(response["changed"], false, "{dialect}: {sql}");
157-
assert!(response.get("reason").is_none(), "{dialect}: {sql}");
158-
assert!(response["unavailable"].is_string(), "{dialect}: {sql}");
159-
count += 1;
160-
}
161-
Ok(count)
162-
}
163-
164148
/// Re-format a layout of an earlier formatter release, which must keep its fingerprint.
165149
fn relaid_previous_layout(previous: &str, dialect: &str) -> Result<String, String> {
166150
let relaid = format_once(previous, dialect, None)?;

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

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use serde_json::Value;
22

3-
use crate::sql_lint::tests::helpers::{check_dialect_corpus, check_unavailable_corpus};
3+
use crate::sql_lint::tests::helpers::check_dialect_corpus;
44
use crate::sql_lint::tests::test_types;
55

66
const FORMAT_CORPUS: &str = include_str!("fixtures/format_corpus.json");
@@ -33,6 +33,12 @@ fn given_dialect_corpus_when_formatting_then_output_layout_and_refusals_are_stab
3333
expected_formatted: 124,
3434
expected_refused: 13,
3535
},
36+
test_types::FormatCorpusTestCase {
37+
description: "Databricks identity corpus",
38+
dialect: "databricks",
39+
expected_formatted: 61,
40+
expected_refused: 1,
41+
},
3642
test_types::FormatCorpusTestCase {
3743
description: "T-SQL identity corpus",
3844
dialect: "tsql",
@@ -53,25 +59,3 @@ fn given_dialect_corpus_when_formatting_then_output_layout_and_refusals_are_stab
5359
}
5460
Ok(())
5561
}
56-
57-
/// Dialects SQLQuack does not format yet are reported unavailable, with every query left as written.
58-
#[test]
59-
fn given_dialects_without_a_native_formatter_when_formatting_then_each_query_is_unavailable()
60-
-> Result<(), String> {
61-
let test_cases = [test_types::UnavailableCorpusTestCase {
62-
description: "Databricks corpus",
63-
dialect: "databricks",
64-
expected_unavailable: 62,
65-
}];
66-
let corpus: Vec<Value> =
67-
serde_json::from_str(FORMAT_CORPUS).map_err(|error| error.to_string())?;
68-
for test_case in &test_cases {
69-
assert_eq!(
70-
check_unavailable_corpus(&corpus, test_case.dialect)?,
71-
test_case.expected_unavailable,
72-
"{}",
73-
test_case.description
74-
);
75-
}
76-
Ok(())
77-
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,10 +1894,10 @@ fn given_each_documented_dialect_when_reading_sql_then_sqlquack_was_built_for_it
18941894
expected_formatted: true,
18951895
},
18961896
test_types::DialectReadersTestCase {
1897-
description: "databricks is checked, and reported unavailable to be formatted",
1897+
description: "databricks is checked and formatted",
18981898
dialect: "databricks",
18991899
expected_linted: true,
1900-
expected_formatted: false,
1900+
expected_formatted: true,
19011901
},
19021902
test_types::DialectReadersTestCase {
19031903
description: "duckdb is checked and formatted",

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,6 @@ pub(crate) struct CrossJoinSubjectTestCase {
159159
pub expected_subject: Option<(&'static str, Option<&'static str>)>,
160160
}
161161

162-
pub(crate) struct UnavailableCorpusTestCase {
163-
pub description: &'static str,
164-
pub dialect: &'static str,
165-
pub expected_unavailable: usize,
166-
}
167-
168162
pub(crate) struct DialectReadersTestCase {
169163
pub description: &'static str,
170164
pub dialect: &'static str,

‎tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,12 @@ def test_given_project_when_checking_then_reports_findings_and_exit_status(
318318
},
319319
),
320320
RewriteTestCase(
321-
description="format leaves a Databricks file, which SQLQuack parses but does not format, unchanged",
321+
description="format lays out a Databricks file",
322322
config='dialect = "databricks"\n',
323323
files={"models/q.sql": "select a from t\n"},
324-
arguments=("format", "--check"),
324+
arguments=("format",),
325325
expected_exit=0,
326-
expected_files={"models/q.sql": "select a from t\n"},
326+
expected_files={"models/q.sql": "SELECT a\nFROM t\n"},
327327
),
328328
RewriteTestCase(
329329
description="format lays out BigQuery and PostgreSQL files",

0 commit comments

Comments
 (0)