diff --git a/core/dbt/contracts/graph/nodes.py b/core/dbt/contracts/graph/nodes.py index 4cc72327332..16f6d5c8262 100644 --- a/core/dbt/contracts/graph/nodes.py +++ b/core/dbt/contracts/graph/nodes.py @@ -19,6 +19,8 @@ from mashumaro.types import SerializableType from dbt import deprecations +from dbt.adapters.base import ConstraintSupport +from dbt.adapters.factory import get_adapter_constraint_support from dbt.artifacts.resources import Analysis as AnalysisResource from dbt.artifacts.resources import ( BaseResource, @@ -469,6 +471,13 @@ 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_past_deprecation_date(self) -> bool: + return ( + self.deprecation_date is not None + and self.deprecation_date < datetime.now().astimezone() + ) + @property def search_name(self): if self.version is None: @@ -570,6 +579,37 @@ def build_contract_checksum(self): data = contract_state.encode("utf-8") self.contract.checksum = hashlib.new("sha256", data).hexdigest() + def same_contract_deleted(self) -> bool: + """ + self: the deleted model node + """ + # If the contract wasn't previously enforced, so contract has not changed + if self.contract.enforced is False: + return True + + # Deleted node is passed its deprecation_date, so deletion does not constitute a contract change + if self.is_past_deprecation_date: + return True + + breaking_changes = [f"Contracted model '{self.unique_id}' was deleted or renamed."] + if self.version is None: + warn_or_error( + UnversionedBreakingChange( + breaking_changes=breaking_changes, + model_name=self.name, + model_file_path=self.original_file_path, + ), + node=self, + ) + return False + else: + raise ( + ContractBreakingChangeError( + breaking_changes=breaking_changes, + node=self, + ) + ) + def same_contract(self, old, adapter_type=None) -> bool: # If the contract wasn't previously enforced: if old.contract.enforced is False and self.contract.enforced is False: @@ -601,10 +641,6 @@ def same_contract(self, old, adapter_type=None) -> bool: # Breaking change: the contract was previously enforced, and it no longer is contract_enforced_disabled = True - # TODO: this avoid the circular imports but isn't ideal - from dbt.adapters.base import ConstraintSupport - from dbt.adapters.factory import get_adapter_constraint_support - constraint_support = get_adapter_constraint_support(adapter_type) column_constraints_exist = False diff --git a/core/dbt/graph/selector_methods.py b/core/dbt/graph/selector_methods.py index 380bad2e273..5cb46134503 100644 --- a/core/dbt/graph/selector_methods.py +++ b/core/dbt/graph/selector_methods.py @@ -719,7 +719,9 @@ def check_modified_contract( ) -> Callable[[Optional[SelectorTarget], SelectorTarget], bool]: # get a function that compares two selector target based on compare method provided def check_modified_contract(old: Optional[SelectorTarget], new: SelectorTarget) -> bool: - if hasattr(new, compare_method): + if new is None and hasattr(old, compare_method + "_deleted"): + return getattr(old, compare_method + "_deleted")() + elif hasattr(new, compare_method): # when old body does not exist or old and new are not the same return not old or not getattr(new, compare_method)(old, adapter_type) # type: ignore else: @@ -785,6 +787,16 @@ def search(self, included_nodes: Set[UniqueId], selector: str) -> Iterator[Uniqu if checker(previous_node, node, **keyword_args): # type: ignore yield unique_id + # checkers that can handle deleted nodes + if checker.__name__ in ["check_modified_contract"]: + # ignore included_nodes, since those cannot contain deleted nodes + for previous_unique_id, previous_node in manifest.nodes.items(): + # detect deleted or renamed node + if previous_unique_id not in self.manifest.nodes.keys(): + # do not yield -- deleted nodes should never be selected for downstream execution + # as they are not part of the current project's manifest + checker(previous_node, None, **keyword_args) # type: ignore + class ResultSelectorMethod(SelectorMethod): def search(self, included_nodes: Set[UniqueId], selector: str) -> Iterator[UniqueId]: