Skip to content

Commit d3eae73

Browse files
authored
feat: dbt adapter allow invalid ref for tests (#5207)
1 parent 08739b8 commit d3eae73

File tree

5 files changed

+127
-4
lines changed

5 files changed

+127
-4
lines changed

sqlmesh/dbt/basemodel.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,22 @@ def tests_ref_source_dependencies(self) -> Dependencies:
245245
dependencies.macros = []
246246
return dependencies
247247

248+
def remove_tests_with_invalid_refs(self, context: DbtContext) -> None:
249+
"""
250+
Removes tests that reference models that do not exist in the context in order to match dbt behavior.
251+
252+
Args:
253+
context: The dbt context this model resides within.
254+
255+
Returns:
256+
None
257+
"""
258+
self.tests = [
259+
test
260+
for test in self.tests
261+
if all(ref in context.refs for ref in test.dependencies.refs)
262+
]
263+
248264
def check_for_circular_test_refs(self, context: DbtContext) -> None:
249265
"""
250266
Checks for direct circular references between two models and raises an exception if found.
@@ -295,6 +311,7 @@ def sqlmesh_model_kwargs(
295311
column_types_override: t.Optional[t.Dict[str, ColumnConfig]] = None,
296312
) -> t.Dict[str, t.Any]:
297313
"""Get common sqlmesh model parameters"""
314+
self.remove_tests_with_invalid_refs(context)
298315
self.check_for_circular_test_refs(context)
299316
model_dialect = self.dialect(context)
300317
model_context = context.context_for_dependencies(

sqlmesh/dbt/context.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from sqlmesh.dbt.manifest import ManifestHelper
1212
from sqlmesh.dbt.target import TargetConfig
1313
from sqlmesh.utils import AttributeDict
14-
from sqlmesh.utils.errors import ConfigError, SQLMeshError
14+
from sqlmesh.utils.errors import ConfigError, SQLMeshError, MissingModelError
1515
from sqlmesh.utils.jinja import (
1616
JinjaGlobalAttribute,
1717
JinjaMacroRegistry,
@@ -265,7 +265,8 @@ def context_for_dependencies(self, dependencies: Dependencies) -> DbtContext:
265265
else:
266266
models[ref] = t.cast(ModelConfig, model)
267267
else:
268-
raise ConfigError(f"Model '{ref}' was not found.")
268+
exception = MissingModelError(ref)
269+
raise exception
269270

270271
for source in dependencies.sources:
271272
if source in self.sources:

sqlmesh/dbt/loader.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from sqlmesh.dbt.project import Project
2424
from sqlmesh.dbt.target import TargetConfig
2525
from sqlmesh.utils import UniqueKeyDict
26-
from sqlmesh.utils.errors import ConfigError
26+
from sqlmesh.utils.errors import ConfigError, MissingModelError
2727
from sqlmesh.utils.jinja import (
2828
JinjaMacroRegistry,
2929
make_jinja_registry,
@@ -162,7 +162,14 @@ def _load_audits(
162162
context.set_and_render_variables(package.variables, package.name)
163163
for test in package.tests.values():
164164
logger.debug("Converting '%s' to sqlmesh format", test.name)
165-
audits[test.name] = test.to_sqlmesh(context)
165+
try:
166+
audits[test.name] = test.to_sqlmesh(context)
167+
except MissingModelError as e:
168+
logger.warning(
169+
"Skipping audit '%s' because model '%s' is not a valid ref",
170+
test.name,
171+
e.model_name,
172+
)
166173

167174
return audits
168175

sqlmesh/utils/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ def __init__(self, message: str | Exception, location: t.Optional[Path] = None)
3333
self.location = Path(location) if isinstance(location, str) else location
3434

3535

36+
class MissingModelError(ConfigError):
37+
"""Raised when a model that is referenced is missing."""
38+
39+
def __init__(self, model_name: str) -> None:
40+
self.model_name = model_name
41+
super().__init__(f"Model '{model_name}' was not found.")
42+
43+
3644
class MissingDependencyError(SQLMeshError):
3745
"""Local environment is missing a required dependency for the given operation"""
3846

tests/dbt/test_model.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import pytest
22

3+
from pathlib import Path
4+
5+
from sqlmesh import Context
36
from sqlmesh.dbt.common import Dependencies
47
from sqlmesh.dbt.context import DbtContext
58
from sqlmesh.dbt.model import ModelConfig
69
from sqlmesh.dbt.test import TestConfig
710
from sqlmesh.utils.errors import ConfigError
11+
from sqlmesh.utils.yaml import YAML
812

913
pytestmark = pytest.mark.dbt
1014

@@ -44,3 +48,89 @@ def test_model_test_circular_references() -> None:
4448
upstream_model.check_for_circular_test_refs(context)
4549
with pytest.raises(ConfigError, match="between tests"):
4650
downstream_model.check_for_circular_test_refs(context)
51+
52+
53+
def test_load_invalid_ref_audit_constraints(tmp_path: Path, caplog) -> None:
54+
yaml = YAML()
55+
dbt_project_dir = tmp_path / "dbt"
56+
dbt_project_dir.mkdir()
57+
dbt_model_dir = dbt_project_dir / "models"
58+
dbt_model_dir.mkdir()
59+
full_model_contents = "SELECT 1 as cola"
60+
full_model_file = dbt_model_dir / "full_model.sql"
61+
with open(full_model_file, "w", encoding="utf-8") as f:
62+
f.write(full_model_contents)
63+
model_schema = {
64+
"version": 2,
65+
"models": [
66+
{
67+
"name": "full_model",
68+
"description": "A full model bad ref for audit and constraints",
69+
"columns": [
70+
{
71+
"name": "cola",
72+
"description": "A column that is used in a ref audit and constraints",
73+
"constraints": [
74+
{
75+
"type": "primary_key",
76+
"columns": ["cola"],
77+
"expression": "ref('not_real_model') (cola)",
78+
}
79+
],
80+
"tests": [
81+
{
82+
"relationships": {
83+
"to": "ref('not_real_model')",
84+
"field": "cola",
85+
"description": "A test that references a model that does not exist",
86+
}
87+
}
88+
],
89+
}
90+
],
91+
}
92+
],
93+
}
94+
model_schema_file = dbt_model_dir / "schema.yml"
95+
with open(model_schema_file, "w", encoding="utf-8") as f:
96+
yaml.dump(model_schema, f)
97+
dbt_project_config = {
98+
"name": "invalid_ref_audit_constraints",
99+
"version": "1.0.0",
100+
"config-version": 2,
101+
"profile": "test",
102+
"model-paths": ["models"],
103+
}
104+
dbt_project_file = dbt_project_dir / "dbt_project.yml"
105+
with open(dbt_project_file, "w", encoding="utf-8") as f:
106+
yaml.dump(dbt_project_config, f)
107+
sqlmesh_config = {
108+
"model_defaults": {
109+
"start": "2025-01-01",
110+
}
111+
}
112+
sqlmesh_config_file = dbt_project_dir / "sqlmesh.yaml"
113+
with open(sqlmesh_config_file, "w", encoding="utf-8") as f:
114+
yaml.dump(sqlmesh_config, f)
115+
dbt_data_dir = tmp_path / "dbt_data"
116+
dbt_data_dir.mkdir()
117+
dbt_data_file = dbt_data_dir / "local.db"
118+
dbt_profile_config = {
119+
"test": {
120+
"outputs": {"duckdb": {"type": "duckdb", "path": str(dbt_data_file)}},
121+
"target": "duckdb",
122+
}
123+
}
124+
db_profile_file = dbt_project_dir / "profiles.yml"
125+
with open(db_profile_file, "w", encoding="utf-8") as f:
126+
yaml.dump(dbt_profile_config, f)
127+
128+
context = Context(paths=dbt_project_dir)
129+
assert (
130+
"Skipping audit 'relationships_full_model_cola__cola__ref_not_real_model_' because model 'not_real_model' is not a valid ref"
131+
in caplog.text
132+
)
133+
fqn = '"local"."main"."full_model"'
134+
assert fqn in context.snapshots
135+
# The audit isn't loaded due to the invalid ref
136+
assert context.snapshots[fqn].model.audits == []

0 commit comments

Comments
 (0)