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
1 change: 1 addition & 0 deletions crates/sqlclean-cli/skill/references/docs/embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ the dialect. Offsets in requests, findings, edits and facts count Unicode charac
| `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 |
Expand Down
4 changes: 4 additions & 0 deletions crates/sqlclean/src/sql_lint/_helpers/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,10 @@ pub(crate) fn lint(request: LintRequest) -> Result<LintResponse, String> {
.config_file
.as_deref()
.unwrap_or(crate::constants::DEFAULT_CONFIG_FILE),
long_literal_guidance: request
.long_literal_guidance
.as_deref()
.unwrap_or(crate::sql_quality::constants::LONG_LITERAL_GUIDANCE),
},
)?);
diagnostics.sort_by_key(|diagnostic| (diagnostic.start, diagnostic.end, diagnostic.code));
Expand Down
3 changes: 3 additions & 0 deletions crates/sqlclean/src/sql_lint/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ pub struct LintRequest {
/// The configuration file remediation text names; SQLClean's own when absent.
#[serde(default)]
pub config_file: Option<String>,
/// The advice `SQLCLEAN044` gives before its limit setting; SQLClean's own when absent.
#[serde(default)]
pub long_literal_guidance: Option<String>,
}

/// The generated wrapper around a test or macro body: its terminal SELECT is ceremonial, and these names are generated.
Expand Down
18 changes: 8 additions & 10 deletions crates/sqlclean/src/sql_quality/_helpers/literals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use polyglot_sql::tokens::TokenType;

use crate::sql_lint::models::LintDiagnostic;
use crate::sql_quality::constants::{
LONG_LITERAL, LONG_LITERAL_GUIDANCE, MAX_LITERAL_LENGTH_KEY, RULES_THRESHOLDS_SECTION,
LONG_LITERAL, MAX_LITERAL_LENGTH_KEY, RULES_THRESHOLDS_SECTION,
};
use crate::sql_quality::models::QualityRequest;
use crate::sql_quality::setting_help::{Setting, setting_help};
Expand All @@ -24,11 +24,7 @@ pub(crate) fn diagnostics(request: &QualityRequest<'_>) -> Vec<LintDiagnostic> {
(length > request.max_literal_length).then(|| LintDiagnostic {
code: LONG_LITERAL.code,
message: Cow::Borrowed(LONG_LITERAL.message),
remediation: Cow::Owned(remediation(
length,
request.max_literal_length,
request.config_file,
)),
remediation: Cow::Owned(remediation(length, request)),
start: token.span.start,
end: token.span.end,
fix: None,
Expand All @@ -38,14 +34,16 @@ pub(crate) fn diagnostics(request: &QualityRequest<'_>) -> Vec<LintDiagnostic> {
.collect()
}

fn remediation(length: usize, limit: usize, config_file: &str) -> String {
fn remediation(length: usize, request: &QualityRequest<'_>) -> String {
format!(
"{LONG_LITERAL_GUIDANCE} {}",
"{} {}",
request.long_literal_guidance,
setting_help(&Setting {
purpose: &format!(
"To allow literals of {length} characters (the current value is {limit})"
"To allow literals of {length} characters (the current value is {})",
request.max_literal_length
),
config_file,
config_file: request.config_file,
section: RULES_THRESHOLDS_SECTION,
key: MAX_LITERAL_LENGTH_KEY,
value: &length.to_string(),
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlclean/src/sql_quality/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ pub(crate) struct QualityRequest<'a> {
pub(crate) fixture_body: bool,
/// The configuration file remediation text tells the user to edit.
pub(crate) config_file: &'a str,
/// The advice a long literal finding gives before its limit setting.
pub(crate) long_literal_guidance: &'a str,
}
21 changes: 20 additions & 1 deletion crates/sqlclean/src/sql_quality/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@ use crate::sql_lint::main::engine::lint_json;
use crate::sql_quality::tests::test_types::QualityRuleTestCase;

pub(crate) fn lint(test_case: &QualityRuleTestCase) -> Result<Vec<Value>, String> {
lint_with_host_fields(test_case, "{}")
}

/// Lint `test_case` with `host_fields`, a JSON object, merged into the request.
pub(crate) fn lint_with_host_fields(
test_case: &QualityRuleTestCase,
host_fields: &str,
) -> Result<Vec<Value>, String> {
let relation_keys: Value =
serde_json::from_str(test_case.relation_keys).map_err(|error| error.to_string())?;
let request: Value = json!({
let host_fields: Value =
serde_json::from_str(host_fields).map_err(|error| error.to_string())?;
let mut request: Value = json!({
"version": 1,
"sql": test_case.sql,
"dialect": "duckdb",
Expand All @@ -15,6 +25,15 @@ pub(crate) fn lint(test_case: &QualityRuleTestCase) -> Result<Vec<Value>, String
"max_ranking_order_by": 2,
"relation_keys": relation_keys,
});
request
.as_object_mut()
.ok_or("the request is not an object")?
.extend(
host_fields
.as_object()
.cloned()
.ok_or("host fields are not an object")?,
);
let response: Value = serde_json::from_str(&lint_json(&request.to_string())?)
.map_err(|error| error.to_string())?;
Ok(response["diagnostics"]
Expand Down
50 changes: 41 additions & 9 deletions crates/sqlclean/src/sql_quality/tests/test_quality_rules.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde_json::Value;

use crate::sql_quality::tests::helpers::{anchors, fixed, lint};
use crate::sql_quality::tests::helpers::{anchors, fixed, lint, lint_with_host_fields};
use crate::sql_quality::tests::test_types::{QualityRemediationTestCase, QualityRuleTestCase};

#[test]
Expand Down Expand Up @@ -345,44 +345,64 @@ fn given_threshold_findings_when_linting_then_remediation_shows_the_exact_settin
description: "long literal points at shortening it and the length setting",
sql: "SELECT 'pending orders awaiting shipment' AS label",
rule: "SQLCLEAN044",
host_fields: "{}",
expected_fragments: &[
"Shorten the literal",
"set this in sqlclean.toml:",
"the current value is 24",
"[rules.thresholds]\n max_literal_length = 32",
],
absent_fragments: &[],
},
QualityRemediationTestCase {
description: "a host's long literal guidance replaces SQLClean's and names its file",
sql: "SELECT 'pending orders awaiting shipment' AS label",
rule: "SQLCLEAN044",
host_fields: r#"{"long_literal_guidance": "Declare it once as a constant.", "config_file": "host.toml"}"#,
expected_fragments: &[
"Declare it once as a constant. To allow literals of 32 characters",
"set this in host.toml:",
],
absent_fragments: &["Shorten the literal", "sqlclean.toml"],
},
QualityRemediationTestCase {
description: "long ranking sort points at a row key and the sort setting",
sql: "SELECT ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.a, o.b, o.order_id) AS rn FROM orders AS o",
rule: "SQLCLEAN043",
host_fields: "{}",
expected_fragments: &[
"unique row key as the final tie-breaker",
"HASH(source_partition, source_offset, line_index) AS row_key",
"the current value is 2",
"[rules.thresholds]\n max_ranking_order_by = 3",
],
absent_fragments: &[],
},
QualityRemediationTestCase {
description: "unproven ranking points at a unique tie-breaker and a built row key",
sql: "SELECT ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.a) AS rn FROM orders AS o",
rule: "SQLCLEAN018",
host_fields: "{}",
expected_fragments: &[
"cover a unique key of the ranked rows",
"a unique, non-null tie-breaker column",
"HASH(source_partition, source_offset, line_index) AS row_key",
],
absent_fragments: &[],
},
];
for test_case in &test_cases {
let diagnostics: Vec<Value> = lint(&QualityRuleTestCase {
description: test_case.description,
sql: test_case.sql,
rule: test_case.rule,
relation_keys: "{}",
expected_anchors: &[],
expected_fixed_sql: None,
})?;
let diagnostics: Vec<Value> = lint_with_host_fields(
&QualityRuleTestCase {
description: test_case.description,
sql: test_case.sql,
rule: test_case.rule,
relation_keys: "{}",
expected_anchors: &[],
expected_fixed_sql: None,
},
test_case.host_fields,
)?;
let remediation: &str = diagnostics
.first()
.and_then(|diagnostic| diagnostic["remediation"].as_str())
Expand All @@ -398,6 +418,18 @@ fn given_threshold_findings_when_linting_then_remediation_shows_the_exact_settin
"{}: {remediation}",
test_case.description
);
let leaked: Vec<&str> = test_case
.absent_fragments
.iter()
.copied()
.filter(|fragment| remediation.contains(fragment))
.collect();
assert_eq!(
leaked,
Vec::<&str>::new(),
"{}: {remediation}",
test_case.description
);
}
Ok(())
}
3 changes: 3 additions & 0 deletions crates/sqlclean/src/sql_quality/tests/test_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ pub(crate) struct QualityRemediationTestCase {
pub(crate) description: &'static str,
pub(crate) sql: &'static str,
pub(crate) rule: &'static str,
/// Extra request fields as a JSON object, as a host sends them.
pub(crate) host_fields: &'static str,
pub(crate) expected_fragments: &'static [&'static str],
pub(crate) absent_fragments: &'static [&'static str],
}

pub(crate) struct JsonParseTestCase {
Expand Down
1 change: 1 addition & 0 deletions website/src/content/docs/docs/embedding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ the dialect. Offsets in requests, findings, edits and facts count Unicode charac
| `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 |
Expand Down
Loading