Skip to content

Commit 8d75270

Browse files
authored
perf(snowflake): cut per-model statement overhead, and write a Rules-failed compile to target/compiled-failed
- Drop and replace only the table kinds a build creates. - Snowflake: per-run `SHOW` schema listings and retention; session-only statements such as `QUERY_TAG` keep them. - Fingerprint rows are appended only when the identity changes. Skipping is off with model migration history: rename matching reads fingerprint times. - Scenario cleanup lists targets by name prefix and reuses confirmed results. - Kept: merge/microbatch column reads (warehouse types), the DuckDB staging drop after a swap, the freshness `LAST_ALTERED` read, the `sqb test` path. - A compile whose Rules fail writes its SQL to `target/compiled-failed/` (with the not-buildable header) and never touches `target/compiled/`. The next passing compile deletes it, and the report names each failing file. `target/compiled-models.txt` is removed. - The compile after a Rules-failed compile runs in full, since that compile writes no SQL tests, functions or audits. - Goldens: - `failure/rule-custom-finding.json`: model SQL moves from `compiled` to `compiled_failed`. - `failure/rule-unused-cte.json`: same, with the header. - `failure/rule-unused-cte-after-variable-and-macro.json`: same, with the header.
1 parent 35821c2 commit 8d75270

116 files changed

Lines changed: 3830 additions & 632 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-cache/src/project_reuse/_helpers/target.rs‎

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::collections::{BTreeMap, HashMap};
44
use std::path::{Path, PathBuf};
55

66
use crate::project_reuse::_helpers::files::file_digest;
7+
use crate::project_reuse::constants::FAILED_COMPILED_DIRECTORY_NAME;
78
use crate::project_reuse::models::{CompileRecord, RecordedArtifact, ReuseRules};
89
use crate::project_snapshot::main::stamp_path::stamp_path;
910
use crate::project_snapshot::models::PathStamp;
@@ -20,10 +21,18 @@ fn compiled_root(project_dir: &Path, rules: &ReuseRules) -> PathBuf {
2021
project_dir.join(&rules.compiled_root)
2122
}
2223

23-
/// Stat every compiled artifact plus `extra_paths`, keyed by path.
24+
/// The failed-compile directory beside the compiled directory.
25+
fn failed_root(project_dir: &Path, rules: &ReuseRules) -> PathBuf {
26+
compiled_root(project_dir, rules).with_file_name(FAILED_COMPILED_DIRECTORY_NAME)
27+
}
28+
29+
/// Stat every compiled and failed-compile artifact plus `extra_paths`, keyed by path.
2430
fn snapshot_target_files(project_dir: &Path, extra_paths: &[&str], rules: &ReuseRules) -> Stamps {
2531
let mut stamps: Stamps = BTreeMap::new();
26-
let mut pending: Vec<PathBuf> = vec![compiled_root(project_dir, rules)];
32+
let mut pending: Vec<PathBuf> = vec![
33+
compiled_root(project_dir, rules),
34+
failed_root(project_dir, rules),
35+
];
2736
while let Some(directory) = pending.pop() {
2837
let Ok(entries) = std::fs::read_dir(&directory) else {
2938
continue;
@@ -58,21 +67,29 @@ fn target_stamps(project_dir: &Path, paths: &[&str], tree: bool, rules: &ReuseRu
5867
if !tree {
5968
return paths.iter().copied().filter_map(keyed_stamp).collect();
6069
}
61-
let prefix: String = compiled_prefix(project_dir, rules);
70+
let prefixes: [String; 2] = tree_prefixes(project_dir, rules);
6271
let extra: Vec<&str> = paths
6372
.iter()
6473
.copied()
65-
.filter(|path| !path.starts_with(prefix.as_str()))
74+
.filter(|path| !in_trees(path, &prefixes))
6675
.collect();
6776
snapshot_target_files(project_dir, &extra, rules)
6877
}
6978

70-
fn compiled_prefix(project_dir: &Path, rules: &ReuseRules) -> String {
71-
format!(
72-
"{}{}",
73-
compiled_root(project_dir, rules).to_string_lossy(),
74-
std::path::MAIN_SEPARATOR
75-
)
79+
/// The compiled and failed-compile directories, each with a trailing separator.
80+
fn tree_prefixes(project_dir: &Path, rules: &ReuseRules) -> [String; 2] {
81+
[
82+
compiled_root(project_dir, rules),
83+
failed_root(project_dir, rules),
84+
]
85+
.map(|root| format!("{}{}", root.to_string_lossy(), std::path::MAIN_SEPARATOR))
86+
}
87+
88+
/// Whether `path` is under one of the `prefixes` directories.
89+
fn in_trees(path: &str, prefixes: &[String]) -> bool {
90+
prefixes
91+
.iter()
92+
.any(|prefix| path.starts_with(prefix.as_str()))
7693
}
7794

7895
/// Whether `stamp` shows what this compile left there, and whether that read its content.
@@ -123,13 +140,13 @@ pub(crate) fn verified_target_files(
123140
since_ns: i64,
124141
rules: &ReuseRules,
125142
) -> Option<Vec<PathStamp>> {
126-
let prefix: String = compiled_prefix(project_dir, rules);
143+
let prefixes: [String; 2] = tree_prefixes(project_dir, rules);
127144
let dag_path: Option<&str> = record.dag_path.as_deref();
128145
let written: bool = record.artifacts_written;
129146
let expected: BTreeMap<&str, &RecordedArtifact> = record
130147
.artifacts
131148
.iter()
132-
.filter(|(path, _)| path.starts_with(prefix.as_str()) || Some(path.as_str()) == dag_path)
149+
.filter(|(path, _)| in_trees(path, &prefixes) || Some(path.as_str()) == dag_path)
133150
.map(|(path, artifact)| (path.as_str(), artifact))
134151
.collect();
135152
let paths: Vec<&str> = expected.keys().copied().collect();

‎crates/sqlbuild-cache/src/project_reuse/constants.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ pub const REUSE_IMAGE_SUFFIX: &str = "image";
88
pub const REUSE_MAX_STORED_IMAGES: usize = 2;
99
/// Prefix of digests computed natively, so they never equal a Python digest.
1010
pub const NATIVE_DIGEST_PREFIX: &str = "b3:";
11+
/// The sibling of the compiled directory that holds a compile whose Rules failed.
12+
pub const FAILED_COMPILED_DIRECTORY_NAME: &str = "compiled-failed";

‎crates/sqlbuild-core/src/cli_text/main/format_compile_text.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ pub fn format_compile_text(report: &CompileTextReport) -> String {
8383
"Project compiled",
8484
&summary,
8585
));
86+
for path in &report.failed_model_sql {
87+
lines.push(format!(" {} {path}", style.muted("Failed SQL:")));
88+
}
8689
lines.push(format!(
8790
" {} {}/",
8891
style.muted("Wrote:"),

‎crates/sqlbuild-core/src/cli_text/models.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ pub struct CompileTextReport {
232232
pub warning_count: usize,
233233
/// The compiled SQL directory relative to `target/`'s parent, without a trailing slash.
234234
pub compiled_dir: String,
235+
/// Each failing model's SQL file under `compiled_dir` when Rules failed, relative like it.
236+
pub failed_model_sql: Vec<String>,
235237
pub manifest: bool,
236238
pub lineage: LineageNote,
237239
}

‎crates/sqlbuild-core/src/cli_text/tests/test_cli_text.rs‎

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -104,27 +104,52 @@ fn given_partial_reasons_when_building_notice_then_groups_by_reason() {
104104

105105
#[test]
106106
fn given_report_when_formatting_then_lists_models_and_summary() {
107-
let test_cases = [CompileTextTestCase {
108-
description: "one plain model with a manifest and unavailable lineage",
109-
report: CompileTextReport {
110-
style: CliStyle { use_color: false },
111-
models: vec![ModelLine {
112-
name: "orders".to_owned(),
113-
failed: false,
114-
column_count: 2,
115-
}],
116-
semantic_notice: None,
117-
diagnostics: Vec::new(),
118-
seed_count: 1,
119-
function_count: 0,
120-
error_count: 0,
121-
warning_count: 0,
122-
compiled_dir: "target/compiled".to_owned(),
123-
manifest: true,
124-
lineage: LineageNote::Unavailable,
107+
let test_cases = [
108+
CompileTextTestCase {
109+
description: "one plain model with a manifest and unavailable lineage",
110+
report: CompileTextReport {
111+
style: CliStyle { use_color: false },
112+
models: vec![ModelLine {
113+
name: "orders".to_owned(),
114+
failed: false,
115+
column_count: 2,
116+
}],
117+
semantic_notice: None,
118+
diagnostics: Vec::new(),
119+
seed_count: 1,
120+
function_count: 0,
121+
error_count: 0,
122+
warning_count: 0,
123+
compiled_dir: "target/compiled".to_owned(),
124+
failed_model_sql: Vec::new(),
125+
manifest: true,
126+
lineage: LineageNote::Unavailable,
127+
},
128+
expected_text: "\nCompile ready 1 model\n\n\u{2514}\u{2500}\u{2500} orders OK 2 columns\n\n\u{2713} Project compiled 1 model, 1 seed, 0 functions, 0 errors, 0 warnings\n Wrote: target/compiled/\n Wrote: target/manifest.json\n Column lineage: unavailable\n",
125129
},
126-
expected_text: "\nCompile ready 1 model\n\n\u{2514}\u{2500}\u{2500} orders OK 2 columns\n\n\u{2713} Project compiled 1 model, 1 seed, 0 functions, 0 errors, 0 warnings\n Wrote: target/compiled/\n Wrote: target/manifest.json\n Column lineage: unavailable\n",
127-
}];
130+
CompileTextTestCase {
131+
description: "a compile whose Rules failed names each failing model's SQL",
132+
report: CompileTextReport {
133+
style: CliStyle { use_color: false },
134+
models: vec![ModelLine {
135+
name: "orders".to_owned(),
136+
failed: true,
137+
column_count: 2,
138+
}],
139+
semantic_notice: None,
140+
diagnostics: Vec::new(),
141+
seed_count: 0,
142+
function_count: 0,
143+
error_count: 1,
144+
warning_count: 0,
145+
compiled_dir: "target/compiled-failed".to_owned(),
146+
failed_model_sql: vec!["target/compiled-failed/models/orders.sql".to_owned()],
147+
manifest: false,
148+
lineage: LineageNote::Silent,
149+
},
150+
expected_text: "\nCompile ready 1 model\n\n\u{2514}\u{2500}\u{2500} orders FAIL 2 columns\n\n\u{2717} Project compiled 1 model, 0 seeds, 0 functions, 1 error, 0 warnings\n Failed SQL: target/compiled-failed/models/orders.sql\n Wrote: target/compiled-failed/\n",
151+
},
152+
];
128153

129154
for test_case in test_cases {
130155
assert_eq!(

‎crates/sqlbuild-python/src/bindings/_helpers/compile_command/report.rs‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use pyo3::types::{
55
PyDict, PyDictMethods, PyList, PySet, PySetMethods, PyString, PyTuple, PyTupleMethods,
66
};
77
use pyo3::{IntoPyObjectExt, pyfunction, wrap_pyfunction};
8+
use sqlbuild_cache::project_reuse::constants::FAILED_COMPILED_DIRECTORY_NAME;
89
use sqlbuild_core::cli_text::main::format_compile_text::format_compile_text as compile_text;
910
use sqlbuild_core::cli_text::main::semantic_coverage_notice::semantic_coverage_notice as notice_text;
1011
use sqlbuild_core::cli_text::models::{
@@ -59,6 +60,7 @@ struct ReportInputs<'py> {
5960
graph: Bound<'py, PyAny>,
6061
project: Bound<'py, PyAny>,
6162
written: Bound<'py, PyAny>,
63+
/// A manifest was requested and written, which a compile whose Rules failed never does.
6264
manifest: bool,
6365
lineage: Bound<'py, PyAny>,
6466
diagnostics: Vec<Bound<'py, PyAny>>,
@@ -71,11 +73,13 @@ impl<'py> ReportInputs<'py> {
7173
fn read(arguments: &KeywordArguments<'py>) -> PyResult<Self> {
7274
let graph: Bound<'py, PyAny> = arguments.value("graph")?;
7375
let lineage_mode: Bound<'py, PyAny> = arguments.value("lineage_mode")?;
76+
let written: Bound<'py, PyAny> = arguments.value("written")?;
77+
let failed_compile: bool = native_field(&written, "failed_compile")?.is_truthy()?;
7478
Ok(Self {
7579
project: native_field(&graph, "project")?,
7680
graph,
77-
written: arguments.value("written")?,
78-
manifest: arguments.flag("manifest")?,
81+
manifest: arguments.flag("manifest")? && !failed_compile,
82+
written,
7983
lineage: arguments.value("lineage")?,
8084
diagnostics: arguments
8185
.value("diagnostics")?
@@ -182,11 +186,15 @@ impl<'py> ReportInputs<'py> {
182186
coverage_notice(&self.semantic_coverage()?, reader)
183187
}
184188

185-
/// `written.target_dir / "compiled"` relative to its `target` directory, POSIX style.
189+
/// `written.target_dir / "compiled"`, or `"compiled-failed"` when Rules failed, relative to its `target` directory, POSIX style.
186190
fn compiled_dir(&self) -> PyResult<Bound<'py, PyAny>> {
187191
let py: Python<'py> = self.py();
188-
let path: Bound<'py, PyAny> =
189-
native_field(&self.written, "target_dir")?.div(COMPILED_DIRECTORY)?;
192+
let directory: &str = if native_field(&self.written, "failed_compile")?.is_truthy()? {
193+
FAILED_COMPILED_DIRECTORY_NAME
194+
} else {
195+
COMPILED_DIRECTORY
196+
};
197+
let path: Bound<'py, PyAny> = native_field(&self.written, "target_dir")?.div(directory)?;
190198
let parts: Bound<'py, PyTuple> = native_field(&path, "parts")?.downcast_into()?;
191199
for (index, part) in parts.iter().enumerate() {
192200
if part.eq(TARGET_DIRECTORY)? {
@@ -198,6 +206,16 @@ impl<'py> ReportInputs<'py> {
198206
}
199207
path.call_method0("as_posix")
200208
}
209+
210+
/// Each failing model's SQL file under `compiled_dir`, relative like it, when Rules failed.
211+
fn failed_model_sql(&self) -> PyResult<Vec<String>> {
212+
let directory: String = self.compiled_dir()?.extract()?;
213+
Ok(native_field(&self.written, "failed_model_sql")?
214+
.extract::<Vec<String>>()?
215+
.into_iter()
216+
.map(|relative| format!("{directory}/{relative}"))
217+
.collect())
218+
}
201219
}
202220

203221
/// `int(value)` of a location field, or `None`.
@@ -392,6 +410,7 @@ pub(crate) fn format_compile_text<'py>(
392410
error_count: inputs.severity_count(ERROR_SEVERITY)?,
393411
warning_count: inputs.severity_count(WARNING_SEVERITY)?,
394412
compiled_dir: reader.text(&inputs.compiled_dir()?)?,
413+
failed_model_sql: inputs.failed_model_sql()?,
395414
manifest: inputs.manifest,
396415
lineage,
397416
};
@@ -1036,6 +1055,12 @@ fn compile_json<'py>(
10361055
),
10371056
],
10381057
)?;
1058+
if native_field(&inputs.written, "failed_compile")?.is_truthy()? {
1059+
artifacts.set_item(
1060+
"failed_model_sql",
1061+
PyList::new(py, inputs.failed_model_sql()?)?,
1062+
)?;
1063+
}
10391064
let report: Bound<'_, PyDict> = dict(
10401065
py,
10411066
vec![

‎crates/sqlbuild-python/src/bindings/_helpers/compile_command/reuse_attempt.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use pyo3::prelude::{
88
};
99
use pyo3::types::{PyDict, PyDictMethods, PyFrozenSet, PyList, PyString};
1010
use pyo3::{IntoPyObjectExt, pyfunction, wrap_pyfunction};
11+
use sqlbuild_cache::project_reuse::constants::FAILED_COMPILED_DIRECTORY_NAME;
1112
use sqlbuild_cli::replay::main::write_launch_record::write_launch_record as write_record_file;
1213
use sqlbuild_core::command_output::constants::INVOCATION_CONTEXT_ENV;
1314

@@ -554,6 +555,7 @@ fn compiled_with_delta<'py>(
554555
.join(TARGET_DIRECTORY_NAME)
555556
.join(COMPILED_DIRECTORY_NAME),
556557
)?;
558+
let failed_root: PathBuf = compiled_root.with_file_name(FAILED_COMPILED_DIRECTORY_NAME);
557559
match delta {
558560
DeltaOutcome::Finished(finished) if !verifying => {
559561
keep_delta_claim();
@@ -595,10 +597,13 @@ fn compiled_with_delta<'py>(
595597
raised: None,
596598
compiled: verify::compiled_digests(&compiled_root),
597599
compiled_root: compiled_root.to_string_lossy().into_owned(),
600+
failed: verify::compiled_digests(&failed_root),
601+
failed_root: failed_root.to_string_lossy().into_owned(),
598602
}),
599603
DeltaOutcome::Raised(error) => verify::hold_expected(verify::Expected {
600604
raised: Some(error.to_string()),
601605
compiled_root: compiled_root.to_string_lossy().into_owned(),
606+
failed_root: failed_root.to_string_lossy().into_owned(),
602607
..verify::Expected::default()
603608
}),
604609
}

0 commit comments

Comments
 (0)