Skip to content

Commit fda1eb9

Browse files
committed
add tests and an error to make sure schema update are safe
1 parent 7df04b0 commit fda1eb9

File tree

4 files changed

+40
-4
lines changed

4 files changed

+40
-4
lines changed

core/dbt/artifacts/schemas/manifest/v12/manifest.py

+10
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,13 @@ def upgrade_schema_version(cls, data):
180180
if manifest_schema_version < cls.dbt_schema_version.version:
181181
data = upgrade_manifest_json(data, manifest_schema_version)
182182
return cls.from_dict(data)
183+
184+
@classmethod
185+
def validate(cls, _):
186+
# When dbt try to load an artifact with additional optional fields
187+
# that are not present in the schema, from_dict will work fine.
188+
# As long as validate is not called, the schema will not be enforced.
189+
# This is intentional, as it allows for safer schema upgrades.
190+
raise RuntimeError(
191+
"The WritableManifest should never be validated directly to allow for schema upgrades."
192+
)

tests/functional/artifacts/test_artifacts.py

+16
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,22 @@ def test_run_and_generate(self, project, manifest_schema_path, run_results_schem
663663
> 0
664664
)
665665

666+
# Test artifact with additional fields load fine
667+
def test_load_artifact(self, project, manifest_schema_path, run_results_schema_path):
668+
catcher = EventCatcher(ArtifactWritten)
669+
results = run_dbt(args=["compile"], callbacks=[catcher.catch])
670+
assert len(results) == 7
671+
manifest_dct = get_artifact(os.path.join(project.project_root, "target", "manifest.json"))
672+
# add a field that is not in the schema
673+
for _, node in manifest_dct["nodes"].items():
674+
node["something_else"] = "something_else"
675+
# load the manifest with the additional field
676+
loaded_manifest = WritableManifest.from_dict(manifest_dct)
677+
678+
# successfully loaded the manifest with the additional field, but the field should not be present
679+
for _, node in loaded_manifest.nodes.items():
680+
assert not hasattr(node, "something_else")
681+
666682

667683
class TestVerifyArtifactsReferences(BaseVerifyProject):
668684
@pytest.fixture(scope="class")

tests/unit/contracts/graph/test_nodes.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,12 @@ def test_basic_compiled_model(basic_compiled_dict, basic_compiled_model):
236236
assert node.is_ephemeral is False
237237

238238

239-
def test_invalid_extra_fields_model(minimal_uncompiled_dict):
240-
bad_extra = minimal_uncompiled_dict
241-
bad_extra["notvalid"] = "nope"
242-
assert_fails_validation(bad_extra, ModelNode)
239+
def test_extra_fields_model_okay(minimal_uncompiled_dict):
240+
extra = minimal_uncompiled_dict
241+
extra["notvalid"] = "nope"
242+
# Model still load fine with extra fields
243+
loaded_model = ModelNode.from_dict(extra)
244+
assert not hasattr(loaded_model, "notvalid")
243245

244246

245247
def test_invalid_bad_type_model(minimal_uncompiled_dict):

tests/unit/contracts/graph/test_nodes_parsed.py

+8
Original file line numberDiff line numberDiff line change
@@ -1961,6 +1961,14 @@ def test_basic_source_definition(
19611961
pickle.loads(pickle.dumps(node))
19621962

19631963

1964+
def test_extra_fields_source_definition_okay(minimum_parsed_source_definition_dict):
1965+
extra = minimum_parsed_source_definition_dict
1966+
extra["notvalid"] = "nope"
1967+
# Model still load fine with extra fields
1968+
loaded_source = SourceDefinition.from_dict(extra)
1969+
assert not hasattr(loaded_source, "notvalid")
1970+
1971+
19641972
def test_invalid_missing(minimum_parsed_source_definition_dict):
19651973
bad_missing_name = minimum_parsed_source_definition_dict
19661974
del bad_missing_name["name"]

0 commit comments

Comments
 (0)