Skip to content

Integrations

Embedding the engine

Lint, format and read statement facts from Rust, in a tool that owns its own configuration.

The sqlclean crate is the engine without the command: the built-in Rules, the formatter and the statement facts. Tools that template, compile or generate SQL can call it in process and keep their own configuration, file discovery and Rule codes.

use sqlclean::sql_lint::constants::LINT_API_VERSION;
use sqlclean::sql_lint::main::lint::lint;
use sqlclean::sql_lint::models::{LintRequest, LintResponse};
fn main() -> Result<(), String> {
let request = LintRequest {
version: LINT_API_VERSION,
sql: "SELECT order_id FROM orders WHERE cancelled_at = NULL LIMIT 10".to_owned(),
dialect: "duckdb".to_owned(),
enabled_rules: Some(vec!["SQLCLEAN001".to_owned(), "SQLCLEAN004".to_owned()]),
..LintRequest::default()
};
let response: LintResponse = lint(request)?;
for diagnostic in &response.diagnostics {
println!("{} {}..{} {}", diagnostic.code, diagnostic.start, diagnostic.end, diagnostic.message);
if let Some(fix) = &diagnostic.fix {
println!(" fix: replace {}..{} with {:?}", fix.start, fix.end, fix.replacement);
}
}
Ok(())
}
SQLCLEAN001 47..48 Comparison with NULL is never true
fix: replace 47..48 with "IS"
SQLCLEAN004 54..59 Row selection is nondeterministic

The crate is not published to crates.io. Depend on it from the GitHub repository, pinned to a release tag or commit:

[dependencies]
sqlclean = { git = "https://github.com/sqlstack-io/sqlclean", tag = "<release tag>" }
Function What it does
sql_lint::main::lint::lint Lints one SQL text with a typed LintRequest
sql_lint::main::engine::lint_json The same, with the request and response as JSON strings
sql_lint::main::batch_engine::lint_batch_json Lints a JSON array of requests in parallel; returns an array of {"Ok": response} or {"Err": message}
sql_lint::main::formatter::format_json Formats SQL from a JSON request
sql_lint::main::catalogue::catalogue Every built-in Rule’s code, message and remediation
sql_lint::main::default_rules::default_rules The codes of the Rules enabled by default
sql_facts::main::file_facts::file_facts The statement facts of a SQL text, as custom Rules read them

lint, lint_json and file_facts return Err with a message when the SQL does not parse under the dialect. Offsets in requests, findings, edits and facts count Unicode characters, not bytes.

version must be LINT_API_VERSION; another value is rejected with unsupported native lint request version 2; expected 1.

Field Default Description
version required LINT_API_VERSION
sql required The SQL text
dialect required A dialect name
enabled_rules the default Rules Rule codes or prefixes to run
ignored_rules [] Rule codes or prefixes to skip
max_literal_length 100 The SQLCLEAN044 limit
max_ranking_order_by 5 The SQLCLEAN043 limit
config_file sqlclean.toml The configuration file name that remediation text refers to
long_literal_guidance SQLClean’s advice The advice SQLCLEAN044 gives before its max_literal_length setting, so a host can point at its own way to share a value
externally_referenced_ctes [] CTE names that something outside the SQL reads, so SQLCLEAN005 does not report them
relation_keys {} Unique keys by table name as the SQL writes it, compared without case; an empty key means the relation holds at most one row, so SQLCLEAN038 does not report a cross join with it and SQLCLEAN018 can prove a ranking order
relation_names {} The relation each placeholder table name stands for, so SQLCLEAN038 messages and exceptions name it
external_identifiers [] Names the host defines outside the SQL, which are not unknown qualifiers
dependency_identifiers [] Placeholder table names that stand for other relations
sql_macro_outputs [] The character range each macro call’s output fills and the relations it passes, so qualification is judged as the macro wrote it
allows_dynamic_output_star false The host proved the output columns, so a top-level SELECT * passes SQLCLEAN021
fixture_harness absent The generated wrapper around a test or macro body; see Template code

A selector that matches no Rule is an error: unknown native lint rule 'SQLCLEAN9'.

A host that lints templated SQL replaces each template expression with a placeholder before linting, and maps offsets back afterwards. The crate exports the placeholder prefixes in constants:

Constant Stands for
INTERPOLATION_PLACEHOLDER_PREFIX An interpolated expression, relation or CTE list; the formatter keeps it in place
MACRO_PARAMETER_PLACEHOLDER_PREFIX A macro parameter; SQLCLEAN028 treats parameter.slot reads as written by the macro
RELATION_PARAMETER_PLACEHOLDER_PREFIX A relation parameter, written <prefix><index>__; SELECT * from it alone passes SQLCLEAN021
VALUE_PARAMETER_PLACEHOLDER_PREFIX A value parameter

Rules never judge a placeholder as an authored name.

fixture_harness describes the query a test runner or macro wraps around a body. When it is present, a terminal SELECT 1 is ceremonial: it passes SQLCLEAN022 and SQLCLEAN035, and SQLCLEAN041 and SQLCLEAN042 do not apply.

Field Description
cte_prefixes Prefixes of the CTE names the harness generates, which SQLCLEAN005 treats as reachable
input_cte_prefixes Prefixes of the CTEs standing in for the body’s inputs
empty_relation The table function an input CTE reads with SELECT * FROM <name>() when it has no rows, which passes SQLCLEAN021

LintDiagnostic.fix holds at most one edit: start, end and replacement. The engine does not say whether a fix is safe. The sqlclean command treats the SQLCLEAN001 fix as unsafe, because turning = NULL into IS NULL changes which rows match, and every other built-in fix as safe. A host that applies fixes makes the same decision. See Safe and unsafe fixes.

let request = r#"{"version": 1, "sql": "select order_id, amount from orders where amount > 0", "dialect": "duckdb", "line_width": 100}"#;
println!("{}", format_json(request)?);
{"version":1,"sql":"SELECT\n order_id,\n amount\nFROM orders\nWHERE amount > 0","changed":true,"formatted":true}

indent sets the spaces per indent step. Without it the engine indents two spaces, its layout before the sqlclean command chose four; pass "indent": 4 to match sqlclean format.

When the formatter cannot format the SQL, formatted is false, sql is the input unchanged, and reason says why.

format_json formats the text it is given as one unit. sqlclean format formats each statement on its own, joins them with one blank line and ends the file with a newline. To produce the same layout, split the file with file_facts, whose statements carry start and terminated_end, and format each statement. See Formatting.

let facts = file_facts("SELECT o.order_id FROM orders AS o", "duckdb")?;
for statement in &facts.statements {
for table in &statement.tables {
println!("{} {} {}..{}", statement.kind, table.qualified_name, table.start, table.end);
}
}
select orders 23..29

The fields are those of Statement facts, without line, column and text. Functions are NamedNode values with a name.

The crate reads no files, no configuration and no cache, and does not run Python. A host owns:

  • which files to lint and in which dialect;
  • Rule selection, which it passes as enabled_rules and ignored_rules;
  • exceptions, ignores and their reasons;
  • the codes and configuration it shows its users;
  • caching, and applying fixes.

The sqlclean command is one such host. Its behaviour, described in the rest of these docs, is a reference for what each of these pieces does.