diff --git a/.changes/unreleased/Features-20230323-133026.yaml b/.changes/unreleased/Features-20230323-133026.yaml new file mode 100644 index 00000000000..67e3297f7d5 --- /dev/null +++ b/.changes/unreleased/Features-20230323-133026.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Detect breaking changes to contracts in state:modified check +time: 2023-03-23T13:30:26.593717-04:00 +custom: + Author: gshank + Issue: "6869" diff --git a/core/dbt/contracts/graph/model_config.py b/core/dbt/contracts/graph/model_config.py index a72b6a57814..60ebf4cd96e 100644 --- a/core/dbt/contracts/graph/model_config.py +++ b/core/dbt/contracts/graph/model_config.py @@ -190,7 +190,7 @@ class Severity(str): @dataclass -class Contract(dbtClassMixin, Replaceable): +class ContractConfig(dbtClassMixin, Replaceable): enforced: bool = False @@ -456,8 +456,8 @@ class NodeConfig(NodeAndTestConfig): default_factory=Docs, metadata=MergeBehavior.Update.meta(), ) - contract: Contract = field( - default_factory=Contract, + contract: ContractConfig = field( + default_factory=ContractConfig, metadata=MergeBehavior.Update.meta(), ) diff --git a/core/dbt/contracts/graph/nodes.py b/core/dbt/contracts/graph/nodes.py index 3598110e221..89c7866a406 100644 --- a/core/dbt/contracts/graph/nodes.py +++ b/core/dbt/contracts/graph/nodes.py @@ -2,6 +2,7 @@ import time from dataclasses import dataclass, field from enum import Enum +import hashlib from mashumaro.types import SerializableType from typing import ( @@ -38,7 +39,7 @@ ) from dbt.contracts.util import Replaceable, AdditionalPropertiesMixin from dbt.events.functions import warn_or_error -from dbt.exceptions import ParsingError, InvalidAccessTypeError +from dbt.exceptions import ParsingError, InvalidAccessTypeError, ModelContractError from dbt.events.types import ( SeedIncreased, SeedExceedsLimitSamePath, @@ -51,7 +52,6 @@ from dbt.node_types import ModelLanguage, NodeType, AccessType from .model_config import ( - Contract, NodeConfig, SeedConfig, TestConfig, @@ -184,6 +184,12 @@ class ColumnInfo(AdditionalPropertiesMixin, ExtensibleDbtClassMixin, Replaceable _extra: Dict[str, Any] = field(default_factory=dict) +@dataclass +class Contract(dbtClassMixin, Replaceable): + enforced: bool = False + checksum: Optional[str] = None + + # Metrics, exposures, @dataclass class HasRelationMetadata(dbtClassMixin, Replaceable): @@ -382,6 +388,13 @@ def same_config(self, old) -> bool: old.unrendered_config, ) + def build_contract_checksum(self): + pass + + def same_contract(self, old) -> bool: + # This would only apply to seeds + return True + def patch(self, patch: "ParsedNodePatch"): """Given a ParsedNodePatch, add the new information to the node.""" # explicitly pick out the parts to update so we don't inadvertently @@ -423,6 +436,7 @@ def same_contents(self, old) -> bool: and self.same_persisted_description(old) and self.same_fqn(old) and self.same_database_representation(old) + and self.same_contract(old) and True ) @@ -492,6 +506,46 @@ def depends_on_nodes(self): def depends_on_macros(self): return self.depends_on.macros + def build_contract_checksum(self): + # We don't need to construct the checksum if the model does not + # have contract enforced, because it won't be used. + # This needs to be executed after contract config is set + if self.contract.enforced is True: + contract_state = "" + # We need to sort the columns so that order doesn't matter + # columns is a str: ColumnInfo dictionary + sorted_columns = sorted(self.columns.values(), key=lambda col: col.name) + for column in sorted_columns: + contract_state += f"|{column.name}" + contract_state += str(column.data_type) + data = contract_state.encode("utf-8") + self.contract.checksum = hashlib.new("sha256", data).hexdigest() + + def same_contract(self, old) -> bool: + if old.contract.enforced is False and self.contract.enforced is False: + # Not a change + return True + if old.contract.enforced is False and self.contract.enforced is True: + # A change, but not a breaking change + return False + + breaking_change_reasons = [] + if old.contract.enforced is True and self.contract.enforced is False: + # Breaking change: throw an error + # Note: we don't have contract.checksum for current node, so build + self.build_contract_checksum() + breaking_change_reasons.append("contract has been disabled") + + if self.contract.checksum != old.contract.checksum: + # Breaking change, throw error + breaking_change_reasons.append("column definitions have changed") + + if breaking_change_reasons: + raise (ModelContractError(reasons=" and ".join(breaking_change_reasons), node=self)) + else: + # no breaking changes + return True + # ==================================== # CompiledNode subclasses @@ -914,7 +968,7 @@ def same_contents(self, old: Optional["SourceDefinition"]) -> bool: if old is None: return True - # config changes are changes (because the only config is "enabled", and + # config changes are changes (because the only config is "enforced", and # enabling a source is a change!) # changing the database/schema/identifier is a change # messing around with external stuff is a change (uh, right?) diff --git a/core/dbt/exceptions.py b/core/dbt/exceptions.py index d85a9443976..ffae0a676e3 100644 --- a/core/dbt/exceptions.py +++ b/core/dbt/exceptions.py @@ -209,6 +209,25 @@ def _fix_dupe_msg(self, path_1: str, path_2: str, name: str, type_name: str) -> ) +class ModelContractError(DbtRuntimeError): + CODE = 10016 + MESSAGE = "Contract Error" + + def __init__(self, reasons, node=None): + self.reasons = reasons + super().__init__(self.message(), node) + + @property + def type(self): + return "Contract" + + def message(self): + return ( + f"There is a breaking change in the model contract because {self.reasons}; " + "you may need to create a new version. See: https://docs.getdbt.com/docs/collaborate/publish/model-versions" + ) + + class RecursionError(DbtRuntimeError): pass diff --git a/core/dbt/graph/selector_methods.py b/core/dbt/graph/selector_methods.py index 4e143ff3c14..af80698c1fe 100644 --- a/core/dbt/graph/selector_methods.py +++ b/core/dbt/graph/selector_methods.py @@ -542,6 +542,7 @@ def search(self, included_nodes: Set[UniqueId], selector: str) -> Iterator[Uniqu ), "modified.relation": self.check_modified_factory("same_database_representation"), "modified.macros": self.check_modified_macros, + "modified.contract": self.check_modified_factory("same_contract"), } if selector in state_checks: checker = state_checks[selector] diff --git a/core/dbt/parser/base.py b/core/dbt/parser/base.py index 229ff22b263..ee9c45e5dc7 100644 --- a/core/dbt/parser/base.py +++ b/core/dbt/parser/base.py @@ -310,8 +310,7 @@ def update_parsed_node_config( else: parsed_node.docs = Docs(show=docs_show) - # If we have contract in the config, copy to node level, for backwards - # compatibility with earlier node-only config. + # If we have contract in the config, copy to node level if "contract" in config_dict and config_dict["contract"]: parsed_node.contract = Contract(enforced=config_dict["contract"]["enforced"]) diff --git a/core/dbt/parser/schemas.py b/core/dbt/parser/schemas.py index 162472ecb07..d9cdce9bed9 100644 --- a/core/dbt/parser/schemas.py +++ b/core/dbt/parser/schemas.py @@ -941,6 +941,7 @@ def parse_patch(self, block: TargetBlock[NodeTarget], refs: ParserRef) -> None: node.patch(patch) self.validate_constraints(node) + node.build_contract_checksum() def validate_constraints(self, patched_node): error_messages = [] diff --git a/schemas/dbt/manifest/v9.json b/schemas/dbt/manifest/v9.json index 9884b2b6eb0..b807caa5076 100644 --- a/schemas/dbt/manifest/v9.json +++ b/schemas/dbt/manifest/v9.json @@ -204,7 +204,7 @@ } }, "additionalProperties": false, - "description": "WritableManifest(metadata: dbt.contracts.graph.manifest.ManifestMetadata, nodes: Mapping[str, Union[dbt.contracts.graph.nodes.AnalysisNode, dbt.contracts.graph.nodes.SingularTestNode, dbt.contracts.graph.nodes.HookNode, dbt.contracts.graph.nodes.ModelNode, dbt.contracts.graph.nodes.RPCNode, dbt.contracts.graph.nodes.SqlNode, dbt.contracts.graph.nodes.GenericTestNode, dbt.contracts.graph.nodes.SnapshotNode, dbt.contracts.graph.nodes.SeedNode]], sources: Mapping[str, dbt.contracts.graph.nodes.SourceDefinition], macros: Mapping[str, dbt.contracts.graph.nodes.Macro], docs: Mapping[str, dbt.contracts.graph.nodes.Documentation], exposures: Mapping[str, dbt.contracts.graph.nodes.Exposure], metrics: Mapping[str, dbt.contracts.graph.nodes.Metric], groups: Mapping[str, dbt.contracts.graph.nodes.Group], selectors: Mapping[str, Any], disabled: Union[Mapping[str, List[Union[dbt.contracts.graph.nodes.AnalysisNode, dbt.contracts.graph.nodes.SingularTestNode, dbt.contracts.graph.nodes.HookNode, dbt.contracts.graph.nodes.ModelNode, dbt.contracts.graph.nodes.RPCNode, dbt.contracts.graph.nodes.SqlNode, dbt.contracts.graph.nodes.GenericTestNode, dbt.contracts.graph.nodes.SnapshotNode, dbt.contracts.graph.nodes.SeedNode, dbt.contracts.graph.nodes.SourceDefinition, dbt.contracts.graph.nodes.Exposure, dbt.contracts.graph.nodes.Metric]]], NoneType], parent_map: Union[Dict[str, List[str]], NoneType], child_map: Union[Dict[str, List[str]], NoneType], group_map: Union[Dict[str, List[str]], NoneType])", + "description": "WritableManifest(metadata: dbt.contracts.graph.manifest.ManifestMetadata, nodes: Mapping[str, Union[dbt.contracts.graph.nodes.AnalysisNode, dbt.contracts.graph.nodes.SingularTestNode, dbt.contracts.graph.nodes.HookNode, dbt.contracts.graph.nodes.ModelNode, dbt.contracts.graph.nodes.RPCNode, dbt.contracts.graph.nodes.SqlNode, dbt.contracts.graph.nodes.GenericTestNode, dbt.contracts.graph.nodes.SnapshotNode, dbt.contracts.graph.nodes.SeedNode]], sources: Mapping[str, dbt.contracts.graph.nodes.SourceDefinition], macros: Mapping[str, dbt.contracts.graph.nodes.Macro], docs: Mapping[str, dbt.contracts.graph.nodes.Documentation], exposures: Mapping[str, dbt.contracts.graph.nodes.Exposure], metrics: Mapping[str, dbt.contracts.graph.nodes.Metric], groups: Mapping[str, dbt.contracts.graph.nodes.Group], selectors: Mapping[str, Any], disabled: Optional[Mapping[str, List[Union[dbt.contracts.graph.nodes.AnalysisNode, dbt.contracts.graph.nodes.SingularTestNode, dbt.contracts.graph.nodes.HookNode, dbt.contracts.graph.nodes.ModelNode, dbt.contracts.graph.nodes.RPCNode, dbt.contracts.graph.nodes.SqlNode, dbt.contracts.graph.nodes.GenericTestNode, dbt.contracts.graph.nodes.SnapshotNode, dbt.contracts.graph.nodes.SeedNode, dbt.contracts.graph.nodes.SourceDefinition, dbt.contracts.graph.nodes.Exposure, dbt.contracts.graph.nodes.Metric]]]], parent_map: Optional[Dict[str, List[str]]], child_map: Optional[Dict[str, List[str]]], group_map: Optional[Dict[str, List[str]]])", "definitions": { "ManifestMetadata": { "type": "object", @@ -221,7 +221,7 @@ "generated_at": { "type": "string", "format": "date-time", - "default": "2023-03-24T13:58:26.458486Z" + "default": "2023-03-28T16:41:19.823746Z" }, "invocation_id": { "oneOf": [ @@ -232,7 +232,7 @@ "type": "null" } ], - "default": "6b8b7103-288b-463d-a739-11578dac0584" + "default": "6830300c-72dd-4d63-a50c-b8cf2df1d379" }, "env": { "type": "object", @@ -375,7 +375,9 @@ "show": true, "node_color": null }, - "contract": {}, + "contract": { + "enforced": false + }, "post-hook": [], "pre-hook": [] } @@ -449,7 +451,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.464077 + "default": 1680021679.82848 }, "config_call_dict": { "type": "object", @@ -546,12 +548,15 @@ "default": [] }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/Contract", + "default": { + "enforced": false, + "checksum": null + } } }, "additionalProperties": false, - "description": "AnalysisNode(database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, build_path: Union[str, NoneType] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Union[str, NoneType] = None, compiled: bool = False, compiled_code: Union[str, NoneType] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Union[str, NoneType] = None, contract: Dict[str, Any] = )" + "description": "AnalysisNode(database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Optional[str] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, build_path: Optional[str] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Optional[str] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Optional[str] = None, compiled: bool = False, compiled_code: Optional[str] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Optional[str] = None, contract: dbt.contracts.graph.nodes.Contract = )" }, "FileHash": { "type": "object", @@ -732,12 +737,14 @@ } }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/ContractConfig", + "default": { + "enforced": false + } } }, "additionalProperties": true, - "description": "NodeConfig(_extra: Dict[str, Any] = , enabled: bool = True, alias: Union[str, NoneType] = None, schema: Union[str, NoneType] = None, database: Union[str, NoneType] = None, tags: Union[List[str], str] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, materialized: str = 'view', incremental_strategy: Union[str, NoneType] = None, persist_docs: Dict[str, Any] = , post_hook: List[dbt.contracts.graph.model_config.Hook] = , pre_hook: List[dbt.contracts.graph.model_config.Hook] = , quoting: Dict[str, Any] = , column_types: Dict[str, Any] = , full_refresh: Union[bool, NoneType] = None, unique_key: Union[str, List[str], NoneType] = None, on_schema_change: Union[str, NoneType] = 'ignore', grants: Dict[str, Any] = , packages: List[str] = , docs: dbt.contracts.graph.unparsed.Docs = , contract: Dict[str, Any] = )" + "description": "NodeConfig(_extra: Dict[str, Any] = , enabled: bool = True, alias: Optional[str] = None, schema: Optional[str] = None, database: Optional[str] = None, tags: Union[List[str], str] = , meta: Dict[str, Any] = , group: Optional[str] = None, materialized: str = 'view', incremental_strategy: Optional[str] = None, persist_docs: Dict[str, Any] = , post_hook: List[dbt.contracts.graph.model_config.Hook] = , pre_hook: List[dbt.contracts.graph.model_config.Hook] = , quoting: Dict[str, Any] = , column_types: Dict[str, Any] = , full_refresh: Optional[bool] = None, unique_key: Union[str, List[str], NoneType] = None, on_schema_change: Optional[str] = 'ignore', grants: Dict[str, Any] = , packages: List[str] = , docs: dbt.contracts.graph.unparsed.Docs = , contract: dbt.contracts.graph.model_config.ContractConfig = )" }, "Hook": { "type": "object", @@ -764,7 +771,7 @@ } }, "additionalProperties": false, - "description": "Hook(sql: str, transaction: bool = True, index: Union[int, NoneType] = None)" + "description": "Hook(sql: str, transaction: bool = True, index: Optional[int] = None)" }, "Docs": { "type": "object", @@ -786,7 +793,19 @@ } }, "additionalProperties": false, - "description": "Docs(show: bool = True, node_color: Union[str, NoneType] = None)" + "description": "Docs(show: bool = True, node_color: Optional[str] = None)" + }, + "ContractConfig": { + "type": "object", + "required": [], + "properties": { + "enforced": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false, + "description": "ContractConfig(enforced: bool = False)" }, "ColumnInfo": { "type": "object", @@ -890,7 +909,7 @@ } }, "additionalProperties": false, - "description": "ColumnLevelConstraint(type: dbt.contracts.graph.nodes.ConstraintType, name: Union[str, NoneType] = None, expression: Union[str, NoneType] = None, warn_unenforced: bool = True, warn_unsupported: bool = True)" + "description": "ColumnLevelConstraint(type: dbt.contracts.graph.nodes.ConstraintType, name: Optional[str] = None, expression: Optional[str] = None, warn_unenforced: bool = True, warn_unsupported: bool = True)" }, "DependsOn": { "type": "object", @@ -931,6 +950,28 @@ "additionalProperties": false, "description": "Used in CompiledNodes as part of ephemeral model processing" }, + "Contract": { + "type": "object", + "required": [], + "properties": { + "enforced": { + "type": "boolean", + "default": false + }, + "checksum": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "description": "Contract(enforced: bool = False, checksum: Optional[str] = None)" + }, "SingularTestNode": { "type": "object", "required": [ @@ -1081,7 +1122,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.466828 + "default": 1680021679.83199 }, "config_call_dict": { "type": "object", @@ -1178,12 +1219,15 @@ "default": [] }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/Contract", + "default": { + "enforced": false, + "checksum": null + } } }, "additionalProperties": false, - "description": "SingularTestNode(database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.TestConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, build_path: Union[str, NoneType] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Union[str, NoneType] = None, compiled: bool = False, compiled_code: Union[str, NoneType] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Union[str, NoneType] = None, contract: Dict[str, Any] = )" + "description": "SingularTestNode(database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.TestConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Optional[str] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, build_path: Optional[str] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Optional[str] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Optional[str] = None, compiled: bool = False, compiled_code: Optional[str] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Optional[str] = None, contract: dbt.contracts.graph.nodes.Contract = )" }, "TestConfig": { "type": "object", @@ -1305,7 +1349,7 @@ } }, "additionalProperties": true, - "description": "TestConfig(_extra: Dict[str, Any] = , enabled: bool = True, alias: Union[str, NoneType] = None, schema: Union[str, NoneType] = 'dbt_test__audit', database: Union[str, NoneType] = None, tags: Union[List[str], str] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, materialized: str = 'test', severity: dbt.contracts.graph.model_config.Severity = 'ERROR', store_failures: Union[bool, NoneType] = None, where: Union[str, NoneType] = None, limit: Union[int, NoneType] = None, fail_calc: str = 'count(*)', warn_if: str = '!= 0', error_if: str = '!= 0')" + "description": "TestConfig(_extra: Dict[str, Any] = , enabled: bool = True, alias: Optional[str] = None, schema: Optional[str] = 'dbt_test__audit', database: Optional[str] = None, tags: Union[List[str], str] = , meta: Dict[str, Any] = , group: Optional[str] = None, materialized: str = 'test', severity: dbt.contracts.graph.model_config.Severity = 'ERROR', store_failures: Optional[bool] = None, where: Optional[str] = None, limit: Optional[int] = None, fail_calc: str = 'count(*)', warn_if: str = '!= 0', error_if: str = '!= 0')" }, "HookNode": { "type": "object", @@ -1392,7 +1436,9 @@ "show": true, "node_color": null }, - "contract": {}, + "contract": { + "enforced": false + }, "post-hook": [], "pre-hook": [] } @@ -1466,7 +1512,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.469568 + "default": 1680021679.8338299 }, "config_call_dict": { "type": "object", @@ -1563,8 +1609,11 @@ "default": [] }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/Contract", + "default": { + "enforced": false, + "checksum": null + } }, "index": { "oneOf": [ @@ -1578,7 +1627,7 @@ } }, "additionalProperties": false, - "description": "HookNode(database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, build_path: Union[str, NoneType] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Union[str, NoneType] = None, compiled: bool = False, compiled_code: Union[str, NoneType] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Union[str, NoneType] = None, contract: Dict[str, Any] = , index: Union[int, NoneType] = None)" + "description": "HookNode(database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Optional[str] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, build_path: Optional[str] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Optional[str] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Optional[str] = None, compiled: bool = False, compiled_code: Optional[str] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Optional[str] = None, contract: dbt.contracts.graph.nodes.Contract = , index: Optional[int] = None)" }, "ModelNode": { "type": "object", @@ -1665,7 +1714,9 @@ "show": true, "node_color": null }, - "contract": {}, + "contract": { + "enforced": false + }, "post-hook": [], "pre-hook": [] } @@ -1739,7 +1790,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.472204 + "default": 1680021679.835818 }, "config_call_dict": { "type": "object", @@ -1836,8 +1887,11 @@ "default": [] }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/Contract", + "default": { + "enforced": false, + "checksum": null + } }, "access": { "type": "string", @@ -1850,7 +1904,7 @@ } }, "additionalProperties": false, - "description": "ModelNode(database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, build_path: Union[str, NoneType] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Union[str, NoneType] = None, compiled: bool = False, compiled_code: Union[str, NoneType] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Union[str, NoneType] = None, contract: Dict[str, Any] = , access: dbt.node_types.AccessType = )" + "description": "ModelNode(database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Optional[str] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, build_path: Optional[str] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Optional[str] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Optional[str] = None, compiled: bool = False, compiled_code: Optional[str] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Optional[str] = None, contract: dbt.contracts.graph.nodes.Contract = , access: dbt.node_types.AccessType = )" }, "RPCNode": { "type": "object", @@ -1937,7 +1991,9 @@ "show": true, "node_color": null }, - "contract": {}, + "contract": { + "enforced": false + }, "post-hook": [], "pre-hook": [] } @@ -2011,7 +2067,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.474336 + "default": 1680021679.8377779 }, "config_call_dict": { "type": "object", @@ -2108,12 +2164,15 @@ "default": [] }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/Contract", + "default": { + "enforced": false, + "checksum": null + } } }, "additionalProperties": false, - "description": "RPCNode(database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, build_path: Union[str, NoneType] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Union[str, NoneType] = None, compiled: bool = False, compiled_code: Union[str, NoneType] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Union[str, NoneType] = None, contract: Dict[str, Any] = )" + "description": "RPCNode(database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Optional[str] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, build_path: Optional[str] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Optional[str] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Optional[str] = None, compiled: bool = False, compiled_code: Optional[str] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Optional[str] = None, contract: dbt.contracts.graph.nodes.Contract = )" }, "SqlNode": { "type": "object", @@ -2200,7 +2259,9 @@ "show": true, "node_color": null }, - "contract": {}, + "contract": { + "enforced": false + }, "post-hook": [], "pre-hook": [] } @@ -2274,7 +2335,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.477368 + "default": 1680021679.839572 }, "config_call_dict": { "type": "object", @@ -2371,12 +2432,15 @@ "default": [] }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/Contract", + "default": { + "enforced": false, + "checksum": null + } } }, "additionalProperties": false, - "description": "SqlNode(database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, build_path: Union[str, NoneType] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Union[str, NoneType] = None, compiled: bool = False, compiled_code: Union[str, NoneType] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Union[str, NoneType] = None, contract: Dict[str, Any] = )" + "description": "SqlNode(database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.NodeConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Optional[str] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, build_path: Optional[str] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Optional[str] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Optional[str] = None, compiled: bool = False, compiled_code: Optional[str] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Optional[str] = None, contract: dbt.contracts.graph.nodes.Contract = )" }, "GenericTestNode": { "type": "object", @@ -2532,7 +2596,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.4802592 + "default": 1680021679.842581 }, "config_call_dict": { "type": "object", @@ -2629,8 +2693,11 @@ "default": [] }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/Contract", + "default": { + "enforced": false, + "checksum": null + } }, "column_name": { "oneOf": [ @@ -2664,7 +2731,7 @@ } }, "additionalProperties": false, - "description": "GenericTestNode(test_metadata: dbt.contracts.graph.nodes.TestMetadata, database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.TestConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, build_path: Union[str, NoneType] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Union[str, NoneType] = None, compiled: bool = False, compiled_code: Union[str, NoneType] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Union[str, NoneType] = None, contract: Dict[str, Any] = , column_name: Union[str, NoneType] = None, file_key_name: Union[str, NoneType] = None, attached_node: Union[str, NoneType] = None)" + "description": "GenericTestNode(test_metadata: dbt.contracts.graph.nodes.TestMetadata, database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.TestConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Optional[str] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, build_path: Optional[str] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Optional[str] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Optional[str] = None, compiled: bool = False, compiled_code: Optional[str] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Optional[str] = None, contract: dbt.contracts.graph.nodes.Contract = , column_name: Optional[str] = None, file_key_name: Optional[str] = None, attached_node: Optional[str] = None)" }, "TestMetadata": { "type": "object", @@ -2691,7 +2758,7 @@ } }, "additionalProperties": false, - "description": "TestMetadata(name: str, kwargs: Dict[str, Any] = , namespace: Union[str, NoneType] = None)" + "description": "TestMetadata(name: str, kwargs: Dict[str, Any] = , namespace: Optional[str] = None)" }, "SnapshotNode": { "type": "object", @@ -2827,7 +2894,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.4844139 + "default": 1680021679.8465102 }, "config_call_dict": { "type": "object", @@ -2924,12 +2991,15 @@ "default": [] }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/Contract", + "default": { + "enforced": false, + "checksum": null + } } }, "additionalProperties": false, - "description": "SnapshotNode(database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.SnapshotConfig, _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, build_path: Union[str, NoneType] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Union[str, NoneType] = None, compiled: bool = False, compiled_code: Union[str, NoneType] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Union[str, NoneType] = None, contract: Dict[str, Any] = )" + "description": "SnapshotNode(database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.SnapshotConfig, _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Optional[str] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, build_path: Optional[str] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Optional[str] = None, raw_code: str = '', language: str = 'sql', refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , compiled_path: Optional[str] = None, compiled: bool = False, compiled_code: Optional[str] = None, extra_ctes_injected: bool = False, extra_ctes: List[dbt.contracts.graph.nodes.InjectedCTE] = , _pre_injected_sql: Optional[str] = None, contract: dbt.contracts.graph.nodes.Contract = )" }, "SnapshotConfig": { "type": "object", @@ -3087,8 +3157,10 @@ } }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/ContractConfig", + "default": { + "enforced": false + } }, "strategy": { "oneOf": [ @@ -3148,7 +3220,7 @@ } }, "additionalProperties": true, - "description": "SnapshotConfig(_extra: Dict[str, Any] = , enabled: bool = True, alias: Union[str, NoneType] = None, schema: Union[str, NoneType] = None, database: Union[str, NoneType] = None, tags: Union[List[str], str] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, materialized: str = 'snapshot', incremental_strategy: Union[str, NoneType] = None, persist_docs: Dict[str, Any] = , post_hook: List[dbt.contracts.graph.model_config.Hook] = , pre_hook: List[dbt.contracts.graph.model_config.Hook] = , quoting: Dict[str, Any] = , column_types: Dict[str, Any] = , full_refresh: Union[bool, NoneType] = None, unique_key: Union[str, NoneType] = None, on_schema_change: Union[str, NoneType] = 'ignore', grants: Dict[str, Any] = , packages: List[str] = , docs: dbt.contracts.graph.unparsed.Docs = , contract: Dict[str, Any] = , strategy: Union[str, NoneType] = None, target_schema: Union[str, NoneType] = None, target_database: Union[str, NoneType] = None, updated_at: Union[str, NoneType] = None, check_cols: Union[str, List[str], NoneType] = None)" + "description": "SnapshotConfig(_extra: Dict[str, Any] = , enabled: bool = True, alias: Optional[str] = None, schema: Optional[str] = None, database: Optional[str] = None, tags: Union[List[str], str] = , meta: Dict[str, Any] = , group: Optional[str] = None, materialized: str = 'snapshot', incremental_strategy: Optional[str] = None, persist_docs: Dict[str, Any] = , post_hook: List[dbt.contracts.graph.model_config.Hook] = , pre_hook: List[dbt.contracts.graph.model_config.Hook] = , quoting: Dict[str, Any] = , column_types: Dict[str, Any] = , full_refresh: Optional[bool] = None, unique_key: Optional[str] = None, on_schema_change: Optional[str] = 'ignore', grants: Dict[str, Any] = , packages: List[str] = , docs: dbt.contracts.graph.unparsed.Docs = , contract: dbt.contracts.graph.model_config.ContractConfig = , strategy: Optional[str] = None, target_schema: Optional[str] = None, target_database: Optional[str] = None, updated_at: Optional[str] = None, check_cols: Union[str, List[str], NoneType] = None)" }, "SeedNode": { "type": "object", @@ -3235,7 +3307,9 @@ "show": true, "node_color": null }, - "contract": {}, + "contract": { + "enforced": false + }, "quote_columns": null, "post-hook": [], "pre-hook": [] @@ -3310,7 +3384,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.489069 + "default": 1680021679.849725 }, "config_call_dict": { "type": "object", @@ -3348,7 +3422,7 @@ } }, "additionalProperties": false, - "description": "SeedNode(database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.SeedConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, build_path: Union[str, NoneType] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, raw_code: str = '', root_path: Union[str, NoneType] = None, depends_on: dbt.contracts.graph.nodes.MacroDependsOn = )" + "description": "SeedNode(database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], alias: str, checksum: dbt.contracts.files.FileHash, config: dbt.contracts.graph.model_config.SeedConfig = , _event_status: Dict[str, Any] = , tags: List[str] = , description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , group: Optional[str] = None, docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, build_path: Optional[str] = None, deferred: bool = False, unrendered_config: Dict[str, Any] = , created_at: float = , config_call_dict: Dict[str, Any] = , relation_name: Optional[str] = None, raw_code: str = '', root_path: Optional[str] = None, depends_on: dbt.contracts.graph.nodes.MacroDependsOn = )" }, "SeedConfig": { "type": "object", @@ -3512,8 +3586,10 @@ } }, "contract": { - "type": "object", - "default": {} + "$ref": "#/definitions/ContractConfig", + "default": { + "enforced": false + } }, "quote_columns": { "oneOf": [ @@ -3527,7 +3603,7 @@ } }, "additionalProperties": true, - "description": "SeedConfig(_extra: Dict[str, Any] = , enabled: bool = True, alias: Union[str, NoneType] = None, schema: Union[str, NoneType] = None, database: Union[str, NoneType] = None, tags: Union[List[str], str] = , meta: Dict[str, Any] = , group: Union[str, NoneType] = None, materialized: str = 'seed', incremental_strategy: Union[str, NoneType] = None, persist_docs: Dict[str, Any] = , post_hook: List[dbt.contracts.graph.model_config.Hook] = , pre_hook: List[dbt.contracts.graph.model_config.Hook] = , quoting: Dict[str, Any] = , column_types: Dict[str, Any] = , full_refresh: Union[bool, NoneType] = None, unique_key: Union[str, List[str], NoneType] = None, on_schema_change: Union[str, NoneType] = 'ignore', grants: Dict[str, Any] = , packages: List[str] = , docs: dbt.contracts.graph.unparsed.Docs = , contract: Dict[str, Any] = , quote_columns: Union[bool, NoneType] = None)" + "description": "SeedConfig(_extra: Dict[str, Any] = , enabled: bool = True, alias: Optional[str] = None, schema: Optional[str] = None, database: Optional[str] = None, tags: Union[List[str], str] = , meta: Dict[str, Any] = , group: Optional[str] = None, materialized: str = 'seed', incremental_strategy: Optional[str] = None, persist_docs: Dict[str, Any] = , post_hook: List[dbt.contracts.graph.model_config.Hook] = , pre_hook: List[dbt.contracts.graph.model_config.Hook] = , quoting: Dict[str, Any] = , column_types: Dict[str, Any] = , full_refresh: Optional[bool] = None, unique_key: Union[str, List[str], NoneType] = None, on_schema_change: Optional[str] = 'ignore', grants: Dict[str, Any] = , packages: List[str] = , docs: dbt.contracts.graph.unparsed.Docs = , contract: dbt.contracts.graph.model_config.ContractConfig = , quote_columns: Optional[bool] = None)" }, "MacroDependsOn": { "type": "object", @@ -3710,11 +3786,11 @@ }, "created_at": { "type": "number", - "default": 1679666306.492453 + "default": 1680021679.8525271 } }, "additionalProperties": false, - "description": "SourceDefinition(database: Union[str, NoneType], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], source_name: str, source_description: str, loader: str, identifier: str, _event_status: Dict[str, Any] = , quoting: dbt.contracts.graph.unparsed.Quoting = , loaded_at_field: Union[str, NoneType] = None, freshness: Union[dbt.contracts.graph.unparsed.FreshnessThreshold, NoneType] = None, external: Union[dbt.contracts.graph.unparsed.ExternalTable, NoneType] = None, description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , source_meta: Dict[str, Any] = , tags: List[str] = , config: dbt.contracts.graph.model_config.SourceConfig = , patch_path: Union[str, NoneType] = None, unrendered_config: Dict[str, Any] = , relation_name: Union[str, NoneType] = None, created_at: float = )" + "description": "SourceDefinition(database: Optional[str], schema: str, name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], source_name: str, source_description: str, loader: str, identifier: str, _event_status: Dict[str, Any] = , quoting: dbt.contracts.graph.unparsed.Quoting = , loaded_at_field: Optional[str] = None, freshness: Optional[dbt.contracts.graph.unparsed.FreshnessThreshold] = None, external: Optional[dbt.contracts.graph.unparsed.ExternalTable] = None, description: str = '', columns: Dict[str, dbt.contracts.graph.nodes.ColumnInfo] = , meta: Dict[str, Any] = , source_meta: Dict[str, Any] = , tags: List[str] = , config: dbt.contracts.graph.model_config.SourceConfig = , patch_path: Optional[str] = None, unrendered_config: Dict[str, Any] = , relation_name: Optional[str] = None, created_at: float = )" }, "Quoting": { "type": "object", @@ -3762,7 +3838,7 @@ } }, "additionalProperties": false, - "description": "Quoting(database: Union[bool, NoneType] = None, schema: Union[bool, NoneType] = None, identifier: Union[bool, NoneType] = None, column: Union[bool, NoneType] = None)" + "description": "Quoting(database: Optional[bool] = None, schema: Optional[bool] = None, identifier: Optional[bool] = None, column: Optional[bool] = None)" }, "FreshnessThreshold": { "type": "object", @@ -3808,7 +3884,7 @@ } }, "additionalProperties": false, - "description": "FreshnessThreshold(warn_after: Union[dbt.contracts.graph.unparsed.Time, NoneType] = , error_after: Union[dbt.contracts.graph.unparsed.Time, NoneType] = , filter: Union[str, NoneType] = None)" + "description": "FreshnessThreshold(warn_after: Optional[dbt.contracts.graph.unparsed.Time] = , error_after: Optional[dbt.contracts.graph.unparsed.Time] = , filter: Optional[str] = None)" }, "FreshnessMetadata": { "type": "object", @@ -3825,7 +3901,7 @@ "generated_at": { "type": "string", "format": "date-time", - "default": "2023-03-24T13:58:26.451737Z" + "default": "2023-03-28T16:41:19.818091Z" }, "invocation_id": { "oneOf": [ @@ -3836,7 +3912,7 @@ "type": "null" } ], - "default": "6b8b7103-288b-463d-a739-11578dac0584" + "default": "6830300c-72dd-4d63-a50c-b8cf2df1d379" }, "env": { "type": "object", @@ -3847,7 +3923,7 @@ } }, "additionalProperties": false, - "description": "FreshnessMetadata(dbt_schema_version: str = , dbt_version: str = '1.5.0b4', generated_at: datetime.datetime = , invocation_id: Union[str, NoneType] = , env: Dict[str, str] = )" + "description": "FreshnessMetadata(dbt_schema_version: str = , dbt_version: str = '1.5.0b4', generated_at: datetime.datetime = , invocation_id: Optional[str] = , env: Dict[str, str] = )" }, "SourceFreshnessRuntimeError": { "type": "object", @@ -3973,7 +4049,7 @@ } }, "additionalProperties": false, - "description": "Time(count: Union[int, NoneType] = None, period: Union[dbt.contracts.graph.unparsed.TimePeriod, NoneType] = None)" + "description": "Time(count: Optional[int] = None, period: Optional[dbt.contracts.graph.unparsed.TimePeriod] = None)" }, "TimingInfo": { "type": "object", @@ -4008,7 +4084,7 @@ } }, "additionalProperties": false, - "description": "TimingInfo(name: str, started_at: Union[datetime.datetime, NoneType] = None, completed_at: Union[datetime.datetime, NoneType] = None)" + "description": "TimingInfo(name: str, started_at: Optional[datetime.datetime] = None, completed_at: Optional[datetime.datetime] = None)" }, "ExternalTable": { "type": "object", @@ -4075,7 +4151,7 @@ } }, "additionalProperties": true, - "description": "ExternalTable(_extra: Dict[str, Any] = , location: Union[str, NoneType] = None, file_format: Union[str, NoneType] = None, row_format: Union[str, NoneType] = None, tbl_properties: Union[str, NoneType] = None, partitions: Union[List[str], List[dbt.contracts.graph.unparsed.ExternalPartition], NoneType] = None)" + "description": "ExternalTable(_extra: Dict[str, Any] = , location: Optional[str] = None, file_format: Optional[str] = None, row_format: Optional[str] = None, tbl_properties: Optional[str] = None, partitions: Union[List[str], List[dbt.contracts.graph.unparsed.ExternalPartition], NoneType] = None)" }, "ExternalPartition": { "type": "object", @@ -4189,7 +4265,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.493356 + "default": 1680021679.853292 }, "supported_languages": { "oneOf": [ @@ -4210,7 +4286,7 @@ } }, "additionalProperties": false, - "description": "Macro(name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, macro_sql: str, depends_on: dbt.contracts.graph.nodes.MacroDependsOn = , description: str = '', meta: Dict[str, Any] = , docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Union[str, NoneType] = None, arguments: List[dbt.contracts.graph.unparsed.MacroArgument] = , created_at: float = , supported_languages: Union[List[dbt.node_types.ModelLanguage], NoneType] = None)" + "description": "Macro(name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, macro_sql: str, depends_on: dbt.contracts.graph.nodes.MacroDependsOn = , description: str = '', meta: Dict[str, Any] = , docs: dbt.contracts.graph.unparsed.Docs = , patch_path: Optional[str] = None, arguments: List[dbt.contracts.graph.unparsed.MacroArgument] = , created_at: float = , supported_languages: Optional[List[dbt.node_types.ModelLanguage]] = None)" }, "MacroArgument": { "type": "object", @@ -4237,7 +4313,7 @@ } }, "additionalProperties": false, - "description": "MacroArgument(name: str, type: Union[str, NoneType] = None, description: str = '')" + "description": "MacroArgument(name: str, type: Optional[str] = None, description: str = '')" }, "Documentation": { "type": "object", @@ -4432,11 +4508,11 @@ }, "created_at": { "type": "number", - "default": 1679666306.496564 + "default": 1680021679.85515 } }, "additionalProperties": false, - "description": "Exposure(name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], type: dbt.contracts.graph.unparsed.ExposureType, owner: dbt.contracts.graph.unparsed.Owner, description: str = '', label: Union[str, NoneType] = None, maturity: Union[dbt.contracts.graph.unparsed.MaturityType, NoneType] = None, meta: Dict[str, Any] = , tags: List[str] = , config: dbt.contracts.graph.model_config.ExposureConfig = , unrendered_config: Dict[str, Any] = , url: Union[str, NoneType] = None, depends_on: dbt.contracts.graph.nodes.DependsOn = , refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , created_at: float = )" + "description": "Exposure(name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], type: dbt.contracts.graph.unparsed.ExposureType, owner: dbt.contracts.graph.unparsed.Owner, description: str = '', label: Optional[str] = None, maturity: Optional[dbt.contracts.graph.unparsed.MaturityType] = None, meta: Dict[str, Any] = , tags: List[str] = , config: dbt.contracts.graph.model_config.ExposureConfig = , unrendered_config: Dict[str, Any] = , url: Optional[str] = None, depends_on: dbt.contracts.graph.nodes.DependsOn = , refs: List[List[str]] = , sources: List[List[str]] = , metrics: List[List[str]] = , created_at: float = )" }, "Owner": { "type": "object", @@ -4464,7 +4540,7 @@ } }, "additionalProperties": true, - "description": "Owner(_extra: Dict[str, Any] = , email: Union[str, NoneType] = None, name: Union[str, NoneType] = None)" + "description": "Owner(_extra: Dict[str, Any] = , email: Optional[str] = None, name: Optional[str] = None)" }, "ExposureConfig": { "type": "object", @@ -4655,7 +4731,7 @@ }, "created_at": { "type": "number", - "default": 1679666306.498284 + "default": 1680021679.857033 }, "group": { "oneOf": [ @@ -4669,7 +4745,7 @@ } }, "additionalProperties": false, - "description": "Metric(name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], description: str, label: str, calculation_method: str, expression: str, filters: List[dbt.contracts.graph.unparsed.MetricFilter], time_grains: List[str], dimensions: List[str], timestamp: Union[str, NoneType] = None, window: Union[dbt.contracts.graph.unparsed.MetricTime, NoneType] = None, model: Union[str, NoneType] = None, model_unique_id: Union[str, NoneType] = None, meta: Dict[str, Any] = , tags: List[str] = , config: dbt.contracts.graph.model_config.MetricConfig = , unrendered_config: Dict[str, Any] = , sources: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , refs: List[List[str]] = , metrics: List[List[str]] = , created_at: float = , group: Union[str, NoneType] = None)" + "description": "Metric(name: str, resource_type: dbt.node_types.NodeType, package_name: str, path: str, original_file_path: str, unique_id: str, fqn: List[str], description: str, label: str, calculation_method: str, expression: str, filters: List[dbt.contracts.graph.unparsed.MetricFilter], time_grains: List[str], dimensions: List[str], timestamp: Optional[str] = None, window: Optional[dbt.contracts.graph.unparsed.MetricTime] = None, model: Optional[str] = None, model_unique_id: Optional[str] = None, meta: Dict[str, Any] = , tags: List[str] = , config: dbt.contracts.graph.model_config.MetricConfig = , unrendered_config: Dict[str, Any] = , sources: List[List[str]] = , depends_on: dbt.contracts.graph.nodes.DependsOn = , refs: List[List[str]] = , metrics: List[List[str]] = , created_at: float = , group: Optional[str] = None)" }, "MetricFilter": { "type": "object", @@ -4724,7 +4800,7 @@ } }, "additionalProperties": false, - "description": "MetricTime(count: Union[int, NoneType] = None, period: Union[dbt.contracts.graph.unparsed.MetricTimePeriod, NoneType] = None)" + "description": "MetricTime(count: Optional[int] = None, period: Optional[dbt.contracts.graph.unparsed.MetricTimePeriod] = None)" }, "MetricConfig": { "type": "object", @@ -4746,7 +4822,7 @@ } }, "additionalProperties": true, - "description": "MetricConfig(_extra: Dict[str, Any] = , enabled: bool = True, group: Union[str, NoneType] = None)" + "description": "MetricConfig(_extra: Dict[str, Any] = , enabled: bool = True, group: Optional[str] = None)" }, "Group": { "type": "object", diff --git a/test/unit/test_contracts_graph_compiled.py b/test/unit/test_contracts_graph_compiled.py index 5ff980a747b..33095574fd0 100644 --- a/test/unit/test_contracts_graph_compiled.py +++ b/test/unit/test_contracts_graph_compiled.py @@ -2,7 +2,6 @@ import pytest from dbt.contracts.files import FileHash -from dbt.contracts.graph.model_config import Contract from dbt.contracts.graph.nodes import ( ColumnInfo, DependsOn, @@ -12,6 +11,7 @@ NodeConfig, TestConfig, TestMetadata, + Contract, ) from dbt.node_types import NodeType diff --git a/tests/functional/artifacts/expected_manifest.py b/tests/functional/artifacts/expected_manifest.py index 7e79210c257..76e3e825d50 100644 --- a/tests/functional/artifacts/expected_manifest.py +++ b/tests/functional/artifacts/expected_manifest.py @@ -322,7 +322,7 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False): "constraints": [], }, }, - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, "patch_path": "test://" + model_schema_yml_path, "docs": {"node_color": None, "show": False}, "compiled": True, @@ -411,7 +411,7 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False): "constraints": [], }, }, - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, "patch_path": "test://" + model_schema_yml_path, "docs": {"node_color": None, "show": False}, "compiled": True, @@ -551,7 +551,7 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False): }, "checksum": {"name": "none", "checksum": ""}, "unrendered_config": unrendered_test_config, - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, }, "snapshot.test.snapshot_seed": { "alias": "snapshot_seed", @@ -563,7 +563,7 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False): "compiled": True, "compiled_code": ANY, "config": snapshot_config, - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, "database": project.database, "group": None, "deferred": False, @@ -612,7 +612,7 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False): "columns": {}, "config": test_config, "group": None, - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, "sources": [], "depends_on": { "macros": ["macro.test.test_nothing", "macro.dbt.get_where_subquery"], @@ -665,7 +665,7 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False): "columns": {}, "config": test_config, "group": None, - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, "sources": [], "depends_on": { "macros": ["macro.dbt.test_unique", "macro.dbt.get_where_subquery"], @@ -928,7 +928,7 @@ def expected_references_manifest(project): "unique_id": "model.test.ephemeral_copy", "compiled": True, "compiled_code": ANY, - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, "extra_ctes_injected": True, "extra_ctes": [], "checksum": checksum_file(ephemeral_copy_path), @@ -961,7 +961,7 @@ def expected_references_manifest(project): }, }, "config": get_rendered_model_config(materialized="table", group="test_group"), - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, "sources": [], "depends_on": {"macros": [], "nodes": ["model.test.ephemeral_copy"]}, "deferred": False, @@ -1023,7 +1023,7 @@ def expected_references_manifest(project): }, }, "config": get_rendered_model_config(), - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, "database": project.database, "depends_on": {"macros": [], "nodes": ["model.test.ephemeral_summary"]}, "deferred": False, @@ -1140,7 +1140,7 @@ def expected_references_manifest(project): "compiled": True, "compiled_code": ANY, "config": get_rendered_snapshot_config(target_schema=alternate_schema), - "contract": {"enforced": False}, + "contract": {"checksum": None, "enforced": False}, "database": model_database, "deferred": False, "depends_on": {"macros": [], "nodes": ["seed.test.seed"]}, diff --git a/tests/functional/defer_state/fixtures.py b/tests/functional/defer_state/fixtures.py index 17f46f842d9..089fe1da349 100644 --- a/tests/functional/defer_state/fixtures.py +++ b/tests/functional/defer_state/fixtures.py @@ -50,6 +50,58 @@ - name: name """ +contract_schema_yml = """ +version: 2 +models: + - name: view_model + columns: + - name: id + tests: + - unique: + severity: error + - not_null + - name: name + - name: table_model + config: + contract: + enforced: True + columns: + - name: id + data_type: integer + tests: + - unique: + severity: error + - not_null + - name: name + data_type: text +""" + +modified_contract_schema_yml = """ +version: 2 +models: + - name: view_model + columns: + - name: id + tests: + - unique: + severity: error + - not_null + - name: name + - name: table_model + config: + contract: + enforced: True + columns: + - name: id + data_type: integer + tests: + - unique: + severity: error + - not_null + - name: user_name + data_type: text +""" + exposures_yml = """ version: 2 exposures: diff --git a/tests/functional/defer_state/test_modified_state.py b/tests/functional/defer_state/test_modified_state.py index 80e3d455da1..a0e3ec1580e 100644 --- a/tests/functional/defer_state/test_modified_state.py +++ b/tests/functional/defer_state/test_modified_state.py @@ -5,9 +5,9 @@ import pytest -from dbt.tests.util import run_dbt, update_config_file, write_file +from dbt.tests.util import run_dbt, update_config_file, write_file, get_manifest -from dbt.exceptions import CompilationError +from dbt.exceptions import CompilationError, ModelContractError from tests.functional.defer_state.fixtures import ( seed_csv, @@ -18,6 +18,8 @@ exposures_yml, macros_sql, infinite_macros_sql, + contract_schema_yml, + modified_contract_schema_yml, ) @@ -261,3 +263,49 @@ def test_changed_exposure(self, project): results = run_dbt(["run", "--models", "+state:modified", "--state", "./state"]) assert len(results) == 1 assert results[0].node.name == "view_model" + + +class TestChangedContract(BaseModifiedState): + def test_changed_contract(self, project): + self.run_and_save_state() + + # update contract for table_model + write_file(contract_schema_yml, "models", "schema.yml") + + # This will find the table_model node modified both through a config change + # and by a non-breaking change to contract: true + results = run_dbt(["run", "--models", "state:modified", "--state", "./state"]) + assert len(results) == 1 + assert results[0].node.name == "table_model" + manifest = get_manifest(project.project_root) + model_unique_id = "model.test.table_model" + model = manifest.nodes[model_unique_id] + expected_unrendered_config = {"contract": {"enforced": True}, "materialized": "table"} + assert model.unrendered_config == expected_unrendered_config + + # Run it again with "state:modified:contract", still finds modified due to contract: true + results = run_dbt(["run", "--models", "state:modified.contract", "--state", "./state"]) + assert len(results) == 1 + manifest = get_manifest(project.project_root) + model = manifest.nodes[model_unique_id] + first_contract_checksum = model.contract.checksum + assert first_contract_checksum + # save a new state + self.copy_state() + + # This should raise because a column name has changed + write_file(modified_contract_schema_yml, "models", "schema.yml") + results = run_dbt(["run"], expect_pass=False) + assert len(results) == 2 + manifest = get_manifest(project.project_root) + model = manifest.nodes[model_unique_id] + second_contract_checksum = model.contract.checksum + # double check different contract_checksums + assert first_contract_checksum != second_contract_checksum + with pytest.raises(ModelContractError): + results = run_dbt(["run", "--models", "state:modified.contract", "--state", "./state"]) + + # Go back to schema file without contract. Should raise an error. + write_file(schema_yml, "models", "schema.yml") + with pytest.raises(ModelContractError): + results = run_dbt(["run", "--models", "state:modified.contract", "--state", "./state"])