Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ 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 = "edfa30b26a0bab86661f96490b86a4f24b92db0a" }
sqlquack-format = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "edfa30b26a0bab86661f96490b86a4f24b92db0a" }
sqlquack-lint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "edfa30b26a0bab86661f96490b86a4f24b92db0a" }
sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "edfa30b26a0bab86661f96490b86a4f24b92db0a" }
sqlquack-fingerprint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }
sqlquack-format = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }
sqlquack-lint = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }
sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }
sqlquack-syntax-sqlserver = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "33e4384967e31d44c2d453640c45ccd222320c93" }

[workspace.lints.rust]
unsafe_code = "forbid"
Expand Down
13 changes: 7 additions & 6 deletions crates/sqlclean-cli/skill/references/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,13 @@ SELECT [order id] FROM orders
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]
```

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

```text
warning: SQL lint is unavailable for the mysql dialect: SQLQuack does not parse it yet
Expand Down
1 change: 1 addition & 0 deletions crates/sqlclean/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ sqlquack-fingerprint.workspace = true
sqlquack-format.workspace = true
sqlquack-lint.workspace = true
sqlquack-syntax.workspace = true
sqlquack-syntax-sqlserver.workspace = true
rayon.workspace = true
rustc-hash.workspace = true
serde.workspace = true
Expand Down
28 changes: 24 additions & 4 deletions crates/sqlclean/src/sql_facts/_helpers/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ use crate::sql_facts::_helpers::tokens::{
function_nodes, join_clause_span, literal_nodes, star_nodes,
};
use crate::sql_facts::constants::{
ALTER_OBJECT_KINDS, CREATE_OBJECT_KINDS, DATE_PART_CALLS, DATE_PART_NAMES, DROP_OBJECT_KINDS,
UTILITY_KINDS,
ALTER_OBJECT_KINDS, AVAILABILITY_PROBE, CREATE_OBJECT_KINDS, DATE_PART_CALLS, DATE_PART_NAMES,
DROP_OBJECT_KINDS, UTILITY_KINDS,
};
use crate::sql_facts::models::{
ColumnNode, FileFacts, JoinNode, NamedNode, StatementFacts, TableNode,
};
use crate::sql_lint::constants::LINT_API_VERSION;
use crate::sql_lint::constants::{LINT_API_VERSION, TSQL_DIALECTS};
use crate::sql_lint::main::lint::lint;
use crate::sql_lint::main::parse_sql::parse_sql;
use crate::sql_lint::types::{LintRequest, LintResponse, ParsedSql};
Expand Down Expand Up @@ -60,6 +60,9 @@ pub(crate) fn file_facts(sql: &str, dialect: &str) -> Result<FileFacts, String>
}

pub(crate) fn statement_ends(sql: &str, dialect: &str) -> Result<Option<Vec<usize>>, String> {
if TSQL_DIALECTS.contains(&dialect.to_ascii_lowercase().as_str()) {
return Ok(Some(sqlserver_statement_ends(sql)));
}
if unavailable(dialect)?.is_some() {
return Ok(None);
}
Expand All @@ -69,10 +72,27 @@ pub(crate) fn statement_ends(sql: &str, dialect: &str) -> Result<Option<Vec<usiz
))
}

/// Why facts are unavailable for `dialect`: lint's own answer for a dialect SQLQuack does not parse yet.
/// 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.
fn sqlserver_statement_ends(sql: &str) -> Vec<usize> {
let ranges = sqlquack_syntax_sqlserver::parse_strict(sql)
.map(|parsed| parsed.ranges)
.unwrap_or_default();
let ends: Vec<usize> = ranges
.iter()
.filter(|range| range.depth == 0)
.map(|range| sql[..range.end].chars().count())
.collect();
match ends.is_empty() {
true => vec![sql.chars().count()],
false => ends,
}
}

/// 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.
fn unavailable(dialect: &str) -> Result<Option<String>, String> {
let probe: LintResponse = lint(LintRequest {
version: LINT_API_VERSION,
sql: AVAILABILITY_PROBE.to_owned(),
dialect: dialect.to_owned(),
enabled_rules: Some(Vec::new()),
..LintRequest::default()
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlclean/src/sql_facts/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,5 @@ pub(crate) const UTILITY_KINDS: [(&str, &str); 11] = [
("REFRESH", "refresh"),
("SHOW", "show"),
];
/// The SQL linted to ask whether SQLQuack parses a dialect; some parsers refuse empty input.
pub(crate) const AVAILABILITY_PROBE: &str = "SELECT 1";
5 changes: 5 additions & 0 deletions crates/sqlclean/src/sql_facts/tests/test_file_facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,11 @@ fn given_a_dialect_sqlquack_does_not_parse_when_collecting_facts_then_they_are_u
dialect: "duckdb",
expected_unavailable: None,
},
UnavailableFactsTestCase {
description: "Databricks parses, though its parser refuses empty input, so its facts are available",
dialect: "databricks",
expected_unavailable: None,
},
];
for test_case in &test_cases {
let facts: FileFacts = file_facts("SELECT 1; SELECT 2", test_case.dialect)?;
Expand Down
9 changes: 7 additions & 2 deletions crates/sqlclean/src/sql_lint/_helpers/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use sqlquack_syntax::Dialect as SqlQuackDialect;
use crate::constants::INTERPOLATION_PLACEHOLDER_PREFIX;
use crate::sql_lint::_helpers::cte_macros::take_out_cte_macros;
use crate::sql_lint::constants::{
DUCKDB_DIALECT, LINT_API_VERSION, SNOWFLAKE_DIALECT, TSQL_DIALECT,
BIGQUERY_DIALECT, DUCKDB_DIALECT, LINT_API_VERSION, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT,
TSQL_DIALECTS,
};
use crate::sql_lint::models::{FormatRequest, FormatResponse};

Expand Down Expand Up @@ -62,7 +63,11 @@ fn sqlquack_dialect(dialect: &str) -> Option<(FormatDialect, Option<SqlQuackDial
match dialect.to_ascii_lowercase().as_str() {
SNOWFLAKE_DIALECT => Some((FormatDialect::Snowflake, Some(SqlQuackDialect::Snowflake))),
DUCKDB_DIALECT => Some((FormatDialect::DuckDB, Some(SqlQuackDialect::DuckDB))),
TSQL_DIALECT => Some((FormatDialect::SqlServer, None)),
BIGQUERY_DIALECT => Some((FormatDialect::BigQuery, None)),
name if POSTGRES_DIALECTS.contains(&name) => {
Some((FormatDialect::PostgreSQL, Some(SqlQuackDialect::PostgreSQL)))
}
name if TSQL_DIALECTS.contains(&name) => Some((FormatDialect::SqlServer, None)),
_ => None,
}
}
Expand Down
8 changes: 6 additions & 2 deletions crates/sqlclean/src/sql_lint/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ pub const LINT_API_VERSION: u32 = 1;
pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake";
/// The dialect name SQLQuack lays out as DuckDB.
pub(crate) const DUCKDB_DIALECT: &str = "duckdb";
/// The dialect name SQLQuack lays out as SQL Server.
pub(crate) const TSQL_DIALECT: &str = "tsql";
/// The dialect name SQLQuack lays out as BigQuery.
pub(crate) const BIGQUERY_DIALECT: &str = "bigquery";
/// The dialect names SQLQuack lays out as PostgreSQL.
pub(crate) const POSTGRES_DIALECTS: [&str; 2] = ["postgres", "postgresql"];
/// The dialect names SQLQuack lays out as SQL Server.
pub(crate) const TSQL_DIALECTS: [&str; 3] = ["tsql", "mssql", "sqlserver"];
Loading
Loading