Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/Features-20260309-163700.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Features
body: Pre-release model versions now warn instead of error on contract breaking changes
time: 2026-03-09T16:37:00.000000-04:00
custom:
Author: jecolvin
Issue: "12164"
27 changes: 27 additions & 0 deletions core/dbt/contracts/graph/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,14 @@ def is_external_node(self) -> bool:
def is_latest_version(self) -> bool:
return self.version is not None and self.version == self.latest_version

@property
def is_prerelease(self) -> bool:
if self.version is None or self.latest_version is None:
return False
from dbt.contracts.graph.unparsed import UnparsedVersion

return UnparsedVersion(v=self.version) > UnparsedVersion(v=self.latest_version)

@property
def is_past_deprecation_date(self) -> bool:
return (
Expand Down Expand Up @@ -692,6 +700,16 @@ def same_contract_removed(self) -> bool:
node=self,
)
return False
elif self.is_prerelease:
warn_or_error(
UnversionedBreakingChange(
breaking_changes=[breaking_change],
model_name=f"{self.name}.v{self.version} (pre-release)",
model_file_path=self.original_file_path,
),
node=self,
)
return False
else:
raise (
ContractBreakingChangeError(
Expand Down Expand Up @@ -912,6 +930,15 @@ def same_contract(self, old, adapter_type=None) -> bool:
),
node=self,
)
elif self.is_prerelease:
warn_or_error(
UnversionedBreakingChange(
breaking_changes=breaking_changes,
model_name=f"{self.name}.v{self.version} (pre-release)",
model_file_path=self.original_file_path,
),
node=self,
)
else:
raise (
ContractBreakingChangeError(
Expand Down
49 changes: 49 additions & 0 deletions tests/functional/defer_state/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,55 @@
data_type: text
"""

prerelease_versioned_contract_schema_yml = """
version: 2
models:
- name: table_model
latest_version: 1
config:
contract:
enforced: True
versions:
- v: 1
- v: 2
columns:
- name: id
data_type: integer
data_tests:
- unique:
severity: error
- not_null
- name: name
data_type: text
"""

prerelease_versioned_modified_contract_schema_yml = """
version: 2
models:
- name: table_model
latest_version: 1
config:
contract:
enforced: True
versions:
- v: 1
- v: 2
columns:
- name: id
data_type: integer
- name: user_name
data_type: text
columns:
- name: id
data_type: integer
data_tests:
- unique:
severity: error
- not_null
- name: name
data_type: text
"""

versioned_modified_contract_schema_yml = """
version: 2
models:
Expand Down
68 changes: 68 additions & 0 deletions tests/functional/defer_state/test_modified_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
no_contract_schema_yml,
numeric_precision_contract_schema_yml,
numeric_precision_increased_contract_schema_yml,
prerelease_versioned_contract_schema_yml,
prerelease_versioned_modified_contract_schema_yml,
schema_yml,
seed_csv,
semantic_model_schema_yml,
Expand Down Expand Up @@ -662,6 +664,72 @@ def test_changed_contract_versioned(self, project):
results = run_dbt(["run", "--models", "state:modified.contract", "--state", "./state"])


class TestChangedContractPrereleaseVersioned(BaseModifiedState):
"""Pre-release versioned models should warn (not error) on breaking contract changes."""

@pytest.fixture(scope="class")
def models(self):
return {
"table_model_v1.sql": table_model_sql,
"table_model_v2.sql": table_model_sql,
"view_model.sql": view_model_sql,
"ephemeral_model.sql": ephemeral_model_sql,
"schema.yml": schema_yml,
"exposures.yml": exposures_yml,
}

def test_changed_contract_prerelease_versioned(self, project):
# Set up with v1 (latest) and v2 (prerelease), both with same columns
write_file(prerelease_versioned_contract_schema_yml, "models", "schema.yml")
self.run_and_save_state()

# Make a breaking change ONLY to v2 (prerelease) via per-version columns
# v1 keeps the original columns, v2 gets a renamed column
write_file(prerelease_versioned_modified_contract_schema_yml, "models", "schema.yml")

# Should NOT raise ContractBreakingChangeError for the prerelease version (v2)
# The run will fail with a compilation error (SQL columns don't match contract)
# but it should NOT raise ContractBreakingChangeError
results = run_dbt(
["run", "--models", "state:modified.contract", "--state", "./state"],
expect_pass=False,
)
# v2 was selected (contract changed) but no ContractBreakingChangeError
assert len(results) == 1
assert results[0].node.name == "table_model"


class TestDeletePrereleaseVersionedContractedModel(BaseModifiedState):
"""Deleting a pre-release versioned model should warn (not error)."""

@pytest.fixture(scope="class")
def models(self):
return {
"table_model_v1.sql": table_model_sql,
"table_model_v2.sql": table_model_sql,
"view_model.sql": view_model_sql,
"ephemeral_model.sql": ephemeral_model_sql,
"schema.yml": schema_yml,
"exposures.yml": exposures_yml,
}

def test_delete_prerelease_versioned_contracted_model(self, project):
# Set up with v1 (latest) and v2 (prerelease)
write_file(prerelease_versioned_contract_schema_yml, "models", "schema.yml")
self.run_and_save_state()

# Delete only the prerelease version (v2) and update schema to only have v1
rm_file(project.project_root, "models", "table_model_v2.sql")
write_file(versioned_contract_schema_yml, "models", "schema.yml")

# Should NOT raise ContractBreakingChangeError for the deleted prerelease version
_, logs = run_dbt_and_capture(
["run", "--models", "state:modified.contract", "--state", "./state"]
)
assert "ContractBreakingChangeError" not in logs
assert "pre-release" in logs.lower()


class TestDeleteUnversionedContractedModel(BaseModifiedState):
MODEL_UNIQUE_ID = "model.test.table_model"
CONTRACT_SCHEMA_YML = contract_schema_yml
Expand Down