Skip to content

Commit 1ecc1d0

Browse files
authored
fix!: batch of refactor, sink, audit, CLI and rules fixes (#1202)
* fix: move and rename a model's unit tests with sqb rename and sqb mv A model's own unit test files, whose every TEST block targets only that model, now follow it: test_<model>[__<behavior>].sql files take the new name, TEST (name "<model>__<behavior>") headers take the new subject, and files in the folder mirroring the model move to the folder mirroring the destination. The edits are verified in the scratch compile like every other edit, reported in --dry-run, and written only when verification passes. * fix: list USING joins and keep lambda parameters in audit-argument column renames An audit argument that joins on the renamed column with USING or NATURAL is now listed for a manual edit instead of failing the verification compile. The column analyser treats a lambda parameter named like the column as its own scope, so references in the lambda body are no longer renamed to the outer column. * fix: resume lifecycle sink delivery after one slow call A sink call over the one-second timeout used to block that sink for the rest of the command, silently dropping every later event including run and invocation outcomes. Delivery now resumes on a fresh worker once the slow call returns; events dropped meanwhile are still counted, outcome events are held and delivered instead of dropped, and the command warns when the stall begins. The timeout clock only charges calls the delivery thread has started and caps each dispatcher round's charge, so time spent waiting for the GIL is not blamed on the sink. The exit warning now points at sink health and the shutdown deadline instead of only the shutdown timeout. * fix!: reject generic audit arguments the audit's SQL never uses An attached generic audit that passes an argument neither its SQL nor its evidence SQL reads, including a misspelt one, now fails the compile with an error naming the audit, the argument to remove and the definition file. Such arguments used to be ignored, so the audit silently checked something other than what the attachment declared. BREAKING CHANGE: projects whose audit attachments pass arguments the audit never reads must remove those arguments. * docs: document the rendered audit SQL compile writes under target/compiled/audits sqb compile already writes each attached generic audit's rendered SQL to target/compiled/audits/generic/<resource>/ and removes it with the attachment, but nothing said so. Document the compiled artifact layout and cover the audit files, and their cleanup, with an end-to-end test. * fix: print expected CLI errors whose text holds undecodable bytes A selector with undecodable bytes made sqb compile crash with a Unicode error instead of reporting S007, in text mode and with --json. Expected error text now keeps lone surrogates as \xNN or \uXXXX escapes before it is printed or put in the JSON report. * fix(rules): stop treating # as a Snowflake line comment The Rules scanner skipped Snowflake code after #, which Snowflake does not treat as a comment (only -- and //), so dependency calls there were missed. The scanner now matches the Snowflake tokenizer. * fix(rules): report SELECT * in plain set-operation branches SQBRMODEL102 only inspected a query's body when it was a single SELECT, so a star in any branch of UNION, INTERSECT or EXCEPT went unreported. The rule now checks every plain SELECT branch of each query body. * test: pin the S101 missing-database message to one settings example * fix: keep an audit's other options in the 64-bit option help The help for an oversized minimum_samples or evidence_limit showed the audit call with only the offending option, so copying it dropped the audit's name, severity and other options. It now repeats the whole call with only the offending value replaced. * fix(rules): exempt each dynamic-pivot SELECT * branch of a set operation SQBRMODEL102 now checks every set-operation branch, but the dynamic-pivot exemption was decided once per query and never matched a set-operation body, so a union of dynamic-pivot star branches was reported per branch although a lone dynamic-pivot star is exempt. The exemption now applies to each branch. * fix: keep delivering lifecycle events when the stall warning cannot be written A failing stderr write of the blocked-sink warning ended the dispatcher thread, leaving queued events undelivered and uncounted. The warning is now best-effort. * fix: keep a unit test in its folder when a move would change what it resolves Test SQL resolves macros, enums and constants from the test file's own folder, so moving a model's test to the destination's mirrored folder could hide a folder-scoped test declaration, failing verification. A test file now moves only when every scoped declaration it uses resolves to the same definition from the destination folder; otherwise it is renamed in place and the output's new notes section names the file and the declaration. The JSON output gains a notes array. * style: satisfy the doc-comment, parameter and test-shape checks for the batch * test: expect escaped errors for undecodable selectors Two integration tests pinned the previous crash (a TypeError) for a selector with undecodable bytes. Expected CLI errors now print that text escaped, so the tests assert the escaped S007 error and a valid ASCII JSON report.
1 parent 2e05617 commit 1ecc1d0

84 files changed

Lines changed: 2038 additions & 136 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/sqlbuild-analysis/src/column_references/_helpers/references.rs‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,17 +346,52 @@ impl Analysis<'_> {
346346
let Some(body) = select_body(expression) else {
347347
return;
348348
};
349+
let shadowed: HashSet<(usize, usize)> = self.lambda_parameter_uses(body);
349350
for node in walk_in_scope(body, false) {
350351
let Expression::Column(column) = node else {
351352
continue;
352353
};
353354
if column.name.name == STAR || column.name.name.to_lowercase() != self.column {
354355
continue;
355356
}
357+
if column.table.is_none()
358+
&& column
359+
.span
360+
.is_some_and(|span| shadowed.contains(&(span.start, span.end)))
361+
{
362+
continue;
363+
}
356364
self.record_column(column, select, chain, role);
357365
}
358366
}
359367

368+
/// Spans of unqualified uses of a lambda parameter named like the column, which read the parameter.
369+
fn lambda_parameter_uses(&self, body: &Expression) -> HashSet<(usize, usize)> {
370+
let mut shadowed: HashSet<(usize, usize)> = HashSet::new();
371+
for node in walk_in_scope(body, false) {
372+
let Expression::Lambda(lambda) = node else {
373+
continue;
374+
};
375+
if !lambda
376+
.parameters
377+
.iter()
378+
.any(|parameter| parameter.name.to_lowercase() == self.column)
379+
{
380+
continue;
381+
}
382+
for inner in walk_in_scope(&lambda.body, false) {
383+
if let Expression::Column(column) = inner
384+
&& column.table.is_none()
385+
&& column.name.name.to_lowercase() == self.column
386+
&& let Some(span) = column.span
387+
{
388+
shadowed.insert((span.start, span.end));
389+
}
390+
}
391+
}
392+
shadowed
393+
}
394+
360395
fn record_column(
361396
&mut self,
362397
column: &polyglot_sql::expressions::Column,

‎crates/sqlbuild-analysis/src/column_references/tests/test_column_references.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ fn given_column_uses_when_analyzing_then_reports_rename_sites() -> Result<(), St
7676
expected_output_aliases: &[],
7777
expected_outputs: 0,
7878
},
79+
ColumnReferenceTestCase {
80+
description: "a lambda parameter named like the column shadows it in the body",
81+
sql: "SELECT list_filter([amount], amount -> amount > 0) AS kept, \
82+
list_filter([1], lambda amount: amount > 0) AS other FROM orders_fact",
83+
output_ctes: &[],
84+
expected_parsed: true,
85+
expected_names: &["amount"],
86+
expected_scopes: &["root"],
87+
expected_bare_projections: 0,
88+
expected_aliases: &[],
89+
expected_star_scopes: &[],
90+
expected_joins: 0,
91+
expected_output_aliases: &[],
92+
expected_outputs: 0,
93+
},
7994
ColumnReferenceTestCase {
8095
description: "output scopes report aliased and bare projections of every branch",
8196
sql: "WITH fixture AS (SELECT 1 AS amount UNION ALL SELECT 2 AS amount) \

‎crates/sqlbuild-attachments/src/audits/_helpers/rendering.rs‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Python's argument merge, SQL rendering and policy resolution for one attachment.
22
3-
use crate::audits::_helpers::parameters::{RenderStop, render_parameterized_sql};
3+
use crate::audits::_helpers::parameters::{
4+
RenderStop, parameter_matches, render_parameterized_sql,
5+
};
46
use crate::audits::_helpers::policies::{resolve_run_scope, resolve_severity};
57
use crate::audits::constants::{MODEL_RESOURCE, SEED_RESOURCE, SOURCE_RESOURCE};
68
use crate::audits::models::{ArgumentValue, AuditAttachment, PolicySource};
@@ -94,3 +96,36 @@ pub(crate) fn merged_arguments(
9496
}
9597
Ok(merged)
9698
}
99+
100+
/// The error for the first explicit argument neither the audit SQL nor its evidence SQL reads.
101+
pub(crate) fn unused_argument(attachment: &AuditAttachment) -> Option<String> {
102+
let mut used: Vec<&str> = parameter_matches(&attachment.sql_body)
103+
.into_iter()
104+
.map(|item| item.name)
105+
.collect();
106+
if let Some(evidence) = &attachment.evidence_sql {
107+
used.extend(
108+
parameter_matches(evidence)
109+
.into_iter()
110+
.map(|item| item.name),
111+
);
112+
}
113+
let implicit: Vec<&str> = attachment
114+
.implicit_arguments
115+
.iter()
116+
.map(|(name, _)| name.as_str())
117+
.collect();
118+
let (name, _) = attachment
119+
.explicit_arguments
120+
.iter()
121+
.find(|(name, _)| !used.contains(&name.as_str()) && !implicit.contains(&name.as_str()))?;
122+
let definition: String = attachment
123+
.definition_path
124+
.as_deref()
125+
.map_or_else(|| "its built-in definition".to_owned(), str::to_owned);
126+
Some(format!(
127+
"{} audit '{}' passes argument '{name}', which the audit's SQL in {definition} never \
128+
uses; remove '{name}' from the attachment",
129+
attachment.owner_label, attachment.definition_name
130+
))
131+
}

‎crates/sqlbuild-attachments/src/audits/main/render_attached_audit.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Attached generic audit rendering: argument merge, SQL, severity and run scope.
22
3-
use crate::audits::_helpers::rendering::{merged_arguments, policies, rendered};
3+
use crate::audits::_helpers::rendering::{merged_arguments, policies, rendered, unused_argument};
44
use crate::audits::models::{ArgumentValue, AuditAttachment, AuditRendering, RenderedAudit};
55

66
/// Render one attachment, or the error rendering raises.
@@ -14,6 +14,9 @@ pub fn render_attached_audit(attachment: &AuditAttachment) -> AuditRendering {
1414
.as_deref()
1515
.map(|evidence| rendered(attachment, evidence, &arguments))
1616
.transpose()?;
17+
if let Some(message) = unused_argument(attachment) {
18+
return Err(message);
19+
}
1720
Ok(RenderedAudit {
1821
sql_body,
1922
evidence_sql,

‎crates/sqlbuild-attachments/src/audits/models.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ pub struct AuditAttachment {
2828
/// Python's `str(owner_file)` and the generic audit name, which its error messages quote.
2929
pub owner_label: String,
3030
pub definition_name: String,
31+
/// The project file defining the audit, or `None` for a built-in audit.
32+
pub definition_path: Option<String>,
3133
pub sql_body: String,
3234
pub evidence_sql: Option<String>,
3335
/// Arguments from the attached resource, such as `model` and `column`; always text.

‎crates/sqlbuild-attachments/src/audits/tests/helpers.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub(super) fn attachment(
4040
AuditAttachment {
4141
owner_label: "models/schema.yml".to_owned(),
4242
definition_name: "accepted_values".to_owned(),
43+
definition_path: None,
4344
sql_body: "SELECT * FROM t WHERE @column NOT IN (@'values')".to_owned(),
4445
evidence_sql: Some("SELECT @column FROM t".to_owned()),
4546
implicit_arguments: vec![("column".to_owned(), "status".to_owned())],

‎crates/sqlbuild-attachments/src/audits/tests/test_render_attached_audit.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ fn given_attached_audits_when_rendering_then_python_rendering_is_returned() {
8080
implicit column from attached context",
8181
),
8282
},
83+
AttachedAuditTestCase {
84+
description: "an argument the audit SQL never reads names itself for removal",
85+
attachment: AuditAttachment {
86+
definition_path: Some("audits/generic/accepted_values.sql".to_owned()),
87+
..attachment(
88+
vec![
89+
values.clone(),
90+
(
91+
"where_condition".to_owned(),
92+
ArgumentValue::Text("status <> 'void'".to_owned()),
93+
),
94+
],
95+
None,
96+
None,
97+
)
98+
},
99+
expected_rendering: failed(
100+
"models/schema.yml audit 'accepted_values' passes argument 'where_condition', \
101+
which the audit's SQL in audits/generic/accepted_values.sql never uses; remove \
102+
'where_condition' from the attachment",
103+
),
104+
},
83105
AttachedAuditTestCase {
84106
description: "a missing argument is Python's error before the policies",
85107
attachment: attachment(Vec::new(), Some("fatal"), None),

‎crates/sqlbuild-cli/src/dispatch/main/event_export_warning.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! The stderr warning for lifecycle events dropped or failed at command exit.
22
3+
use sqlbuild_core::event_export::constants::DEFAULT_EVENT_EXPORT_INVOCATION_TIMEOUT_SECONDS;
4+
35
use crate::dispatch::constants::SHUTDOWN_TIMEOUT_SETTING;
46
use crate::dispatch::models::{ExportSummary, ExporterLoss};
57

@@ -32,7 +34,9 @@ pub fn event_export_warning(summary: &ExportSummary, command_succeeded: bool) ->
3234
};
3335
Some(format!(
3436
"{prefix}: {} of {} {unit} dropped, {} failed ({sink_label} {names}). \
35-
Increase {SHUTDOWN_TIMEOUT_SETTING} or check sink health.",
37+
Check sink health: a sink call slower than {DEFAULT_EVENT_EXPORT_INVOCATION_TIMEOUT_SECONDS}s \
38+
drops that sink's events until it returns; events still queued at exit need a longer \
39+
{SHUTDOWN_TIMEOUT_SETTING}.",
3640
summary.dropped, summary.accepted, summary.failed
3741
))
3842
}

‎crates/sqlbuild-cli/src/dispatch/tests/test_event_export_warning.rs‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ fn given_final_export_summary_when_formatting_warning_then_reports_loss_only() {
1818
expected_warning: Some(
1919
"Warning: command completed successfully, but lifecycle event export was \
2020
incomplete: 140 of 248 events dropped, 0 failed (sink 'orders_events'). \
21-
Increase sinks.lifecycle.shutdown_timeout or check sink health.",
21+
Check sink health: a sink call slower than 1s drops that sink's events until it \
22+
returns; events still queued at exit need a longer \
23+
sinks.lifecycle.shutdown_timeout.",
2224
),
2325
},
2426
EventExportWarningTestCase {
@@ -28,7 +30,9 @@ fn given_final_export_summary_when_formatting_warning_then_reports_loss_only() {
2830
expected_warning: Some(
2931
"Warning: lifecycle event export was incomplete: 0 of 10 events dropped, \
3032
1 failed (sink 'orders_events'). \
31-
Increase sinks.lifecycle.shutdown_timeout or check sink health.",
33+
Check sink health: a sink call slower than 1s drops that sink's events until it \
34+
returns; events still queued at exit need a longer \
35+
sinks.lifecycle.shutdown_timeout.",
3236
),
3337
},
3438
EventExportWarningTestCase {
@@ -41,7 +45,9 @@ fn given_final_export_summary_when_formatting_warning_then_reports_loss_only() {
4145
expected_warning: Some(
4246
"Warning: command completed successfully, but lifecycle event export was \
4347
incomplete: 4 of 40 event deliveries dropped, 1 failed (sink 'orders_events'). \
44-
Increase sinks.lifecycle.shutdown_timeout or check sink health.",
48+
Check sink health: a sink call slower than 1s drops that sink's events until it \
49+
returns; events still queued at exit need a longer \
50+
sinks.lifecycle.shutdown_timeout.",
4551
),
4652
},
4753
];

‎crates/sqlbuild-model-config/src/header_metadata/_helpers/audits.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ fn configured_audit<N: AuthoredNode>(
9090
let minimum_samples = optional_count(
9191
entry(options, "minimum_samples"),
9292
option_site,
93-
(definition, "minimum_samples"),
93+
(definition, "minimum_samples", options),
9494
)?;
9595
let evidence_limit = optional_count(
9696
entry(options, "evidence_limit"),
9797
option_site,
98-
(definition, "evidence_limit"),
98+
(definition, "evidence_limit", options),
9999
)?;
100100
let meta = optional_meta(entry(options, "meta"), option_site)?;
101101
let arguments: Vec<(N, N)> = options

0 commit comments

Comments
 (0)