Skip to content

Commit 7e908ac

Browse files
authored
fix(test): resolve mocked and expected-row references consistently in SQL tests (#1023)
* fix(test): resolve mocked and expected-row references consistently in SQL tests A mocked name now always means the mock in every SQL-test CTE: an assertion calling __ref() on a mocked model reads the mock instead of running the real model, and mocked models no longer count as test targets. References written directly in __expected__ CTEs are resolved like assertions and helpers. Compile reports unmocked __source(), __seed() and __dbt_ref() calls and unknown models in expected rows with P013. The native planner computes the models helpers and expected rows read itself and rejects any reference left raw. * fix(test): reject SQL tests whose checks read only mocks A model-mode SQL test whose expected rows and assertions read only mocked names runs no model and used to be skipped silently. Compile now reports it with a located P013 diagnostic showing how to call __ref() on the model under test. Macro, UDF and table-function tests and scenarios keep their own targets. * fix(test): plan SQL-test helpers from the compiler's reads and clarify mock-only errors The native planner now takes the helpers a test reads and the models its expected rows and helpers reference from the compiler, fails when a request omits them, and leaves unread helpers that call references out of the test query, so a helper named like a column no longer runs or fails a test. A reference the planner still cannot resolve is a located P013 instead of an unlocated P001. The error for a test that mocks the model it tests now explains how to mock the model's inputs instead. * fix(test): satisfy the manual_contains lint in mocked reference checks
1 parent a281c0d commit 7e908ac

24 files changed

Lines changed: 1612 additions & 299 deletions

File tree

‎crates/sqlbuild-analysis/src/compiler/_helpers/sql_tests/helper_scope.rs‎

Lines changed: 33 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::compiler::_helpers::sql_tests::planning::{
1515
DBT_REF_PREFIX, REF_PREFIX, SEED_PREFIX, SOURCE_PREFIX, SqlTestPatterns, TABLE_FUNCTION_PREFIX,
1616
TestFixtures, compile_error,
1717
};
18+
use crate::compiler::_helpers::sql_tests::reader_references::calls_reference;
1819
use crate::constants::{
1920
QUOTED_IDENTIFIER_DELIMITER_BYTES, SQL_TEST_ACTUAL_CTE, SQL_TEST_ACTUAL_CTE_PREFIX,
2021
SQL_TEST_EXPECTED_CTE,
@@ -80,6 +81,8 @@ pub(crate) struct ScopeRequest<'a> {
8081
pub(crate) rename_dialect: Option<&'a Dialect>,
8182
pub(crate) flat_dialect: bool,
8283
pub(crate) slice_dialect: SliceDialect,
84+
/// Helpers the compiler found the test reads; an unread helper calling a reference is left out.
85+
pub(crate) read_helper_names: &'a [String],
8386
}
8487

8588
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
@@ -160,13 +163,17 @@ impl ScopeGraph {
160163
.mocks_by_generated_name
161164
.insert(mock.generated_name.clone(), index);
162165
}
163-
if fixtures.helpers.is_empty() {
164-
return Ok(graph);
165-
}
166-
for (index, cte) in fixtures.helpers.iter().enumerate() {
167-
graph
168-
.helper_indexes
169-
.insert(cte.name.to_ascii_lowercase(), index);
166+
let read: HashSet<String> = request
167+
.read_helper_names
168+
.iter()
169+
.map(|name| name.to_ascii_lowercase())
170+
.collect();
171+
for cte in &fixtures.helpers {
172+
let key = cte.name.to_ascii_lowercase();
173+
if !read.contains(&key) && calls_reference(&cte.sql_body, patterns) {
174+
continue;
175+
}
176+
graph.helper_indexes.insert(key, graph.helpers.len());
170177
graph.helpers.push(ScopeHelper {
171178
name: cte.name.clone(),
172179
original: cte.sql_body.clone(),
@@ -176,6 +183,9 @@ impl ScopeGraph {
176183
resolved: None,
177184
});
178185
}
186+
if graph.helpers.is_empty() {
187+
return Ok(graph);
188+
}
179189
let readers = test_readers(fixtures);
180190
graph.reject_shadowed_helpers(&readers, request)?;
181191
if let Some(dialect) = request.rename_dialect
@@ -294,36 +304,12 @@ impl ScopeGraph {
294304
order.ordered
295305
}
296306

297-
/// Helpers the test's expected rows and assertions read, except those a mock it uses reads.
298-
pub(crate) fn read_helpers(
299-
&self,
300-
readers: &[&str],
301-
used_mocks: &HashSet<String>,
302-
patterns: &SqlTestPatterns,
303-
) -> HashSet<usize> {
304-
let helpers = |nodes: Vec<ScopeNode>| -> HashSet<usize> {
305-
nodes
306-
.into_iter()
307-
.filter_map(|node| match node {
308-
ScopeNode::Helper(index) => Some(index),
309-
ScopeNode::Mock(_) => None,
310-
})
311-
.collect()
312-
};
313-
let mut read: HashSet<usize> = HashSet::new();
314-
for sql in readers {
315-
read.extend(helpers(
316-
self.closure(&identifier_tokens(sql, patterns), true),
317-
));
318-
}
319-
for mock in &self.mocks {
320-
if used_mocks.contains(&mock.mock_name) {
321-
for index in helpers(self.closure(&mock.tokens, true)) {
322-
read.remove(&index);
323-
}
324-
}
325-
}
326-
read
307+
/// Indexes of the helpers the compiler found the test's expected rows and assertions read.
308+
pub(crate) fn read_helper_indexes(&self, names: &[String]) -> HashSet<usize> {
309+
names
310+
.iter()
311+
.filter_map(|name| self.helper_indexes.get(&name.to_ascii_lowercase()).copied())
312+
.collect()
327313
}
328314

329315
/// Read helpers whose SQL, as placed in the test query, calls a reference such as `__ref()`.
@@ -344,6 +330,11 @@ impl ScopeGraph {
344330
.collect()
345331
}
346332

333+
/// The authored name of one helper.
334+
pub(crate) fn helper_name(&self, index: usize) -> &str {
335+
&self.helpers[index].name
336+
}
337+
347338
/// Replace a helper's references with the CTEs a reader must define ahead of it.
348339
pub(crate) fn resolve_helper(&mut self, index: usize, resolved: ResolvedHelper) {
349340
let placement = self.placement;
@@ -549,35 +540,21 @@ pub(crate) fn helper_scope_ctes(
549540
Ok(graph.collect_scope(nodes))
550541
}
551542

552-
/// Merge scoped CTEs into a step's CTEs; relations resolved helpers read come first and win.
543+
/// Append scoped CTEs after a step's CTEs; a same-named mock or model CTE keeps the step's copy.
553544
pub(crate) fn merged_scoped_ctes(
554545
lifted: Vec<(String, String)>,
555546
scoped: HelperScope,
556547
file_label: &str,
557548
) -> Result<Vec<(String, String)>, String> {
558-
type Ctes = Vec<(String, String)>;
559-
let (generated, authored): (Ctes, Ctes) = scoped
560-
.ctes
561-
.into_iter()
562-
.partition(|(name, _)| scoped.generated.contains(&name.to_ascii_lowercase()));
563549
let mut merged: Vec<(String, String)> = lifted;
564-
if !generated.is_empty() {
565-
let own = std::mem::replace(&mut merged, generated);
566-
for (name, sql) in own {
567-
if !merged
568-
.iter()
569-
.any(|(existing, _)| existing.eq_ignore_ascii_case(&name))
570-
{
571-
merged.push((name, sql));
572-
}
573-
}
574-
}
575-
for (name, sql) in authored {
550+
for (name, sql) in scoped.ctes {
576551
match merged
577552
.iter()
578553
.find(|(existing, _)| existing.eq_ignore_ascii_case(&name))
579554
{
580-
Some((_, existing_sql)) if *existing_sql == sql => {}
555+
Some((_, existing_sql))
556+
if *existing_sql == sql
557+
|| scoped.generated.contains(&name.to_ascii_lowercase()) => {}
581558
Some(_) => {
582559
return Err(compile_error(&format!(
583560
"SQL test '{file_label}' defines CTE '{name}', which conflicts with the generated CTE"

‎crates/sqlbuild-analysis/src/compiler/_helpers/sql_tests/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod helper_names;
77
pub mod helper_scope;
88
pub mod markers;
99
pub mod planning;
10+
pub mod reader_references;
1011
pub mod relation_markers;
1112
pub mod rendering;
1213
pub mod scenario_extraction;

0 commit comments

Comments
 (0)