Skip to content

Commit 7f3ea39

Browse files
drop model freshness as top level model property (in favor of config freshness) (#11731) (#11736)
* Begin testing that model freshness can't be set as a top level model property * Remove ability to specify freshness as top level property of models * Add come comments to calculate_node_config for better readability * Drop `freshness` as a top level property of models, and let `patch_node_config` handle merging config freshness Model freshness hasn't been released in a minor release yet, not been documented. Thus it is safe to remove the top level property of freshness on models. Freshness will instead be set, and gotten, from the model config. Additionally our way of calculating the config model freshness only got the top level `+freshness` from dbt_project.yml (ignoring any path specific definitions). By instead using the built in `calculate_node_config` (which is eventually called by `patch_node_config`), we get all path specific freshness config handling and it also handles the precedence of `dbt_project.yml` specification, schema file specification, and sql file specification. * add changie doc (cherry picked from commit 091ba5f) Co-authored-by: Quigley Malcolm <[email protected]>
1 parent 7c8d98d commit 7f3ea39

File tree

9 files changed

+55
-73
lines changed

9 files changed

+55
-73
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Fixes
2+
body: "Remove model freshness property support in favor of config level support"
3+
time: 2025-06-16T08:56:00.641553-05:00
4+
custom:
5+
Author: QMalcolm
6+
Issue: "11713"

core/dbt/artifacts/resources/v1/model.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ class Model(CompiledResource):
114114
defer_relation: Optional[DeferRelation] = None
115115
primary_key: List[str] = field(default_factory=list)
116116
time_spine: Optional[TimeSpine] = None
117-
freshness: Optional[ModelFreshness] = None
118117

119118
def __post_serialize__(self, dct: Dict, context: Optional[Dict] = None):
120119
dct = super().__post_serialize__(dct, context)

core/dbt/context/context_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,21 @@ def initial_result(self, resource_type: NodeType, base: bool) -> T: ...
141141

142142
def calculate_node_config(
143143
self,
144+
# this is the config from the sql file
144145
config_call_dict: Dict[str, Any],
145146
fqn: List[str],
146147
resource_type: NodeType,
147148
project_name: str,
148149
base: bool,
150+
# this is the config from the schema file
149151
patch_config_dict: Optional[Dict[str, Any]] = None,
150152
) -> BaseConfig:
151153
own_config = self.get_node_project(project_name)
152154

153155
result = self.initial_result(resource_type=resource_type, base=base)
154156

157+
# builds the config from what was specified in the runtime_config, which generally
158+
# comes from the project's dbt_project.yml file.
155159
project_configs = self._project_configs(own_config, fqn, resource_type)
156160
for fqn_config in project_configs:
157161
result = self._update_from_config(result, fqn_config)

core/dbt/contracts/graph/unparsed.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,6 @@ class UnparsedModelUpdate(UnparsedNodeUpdate):
225225
versions: Sequence[UnparsedVersion] = field(default_factory=list)
226226
deprecation_date: Optional[datetime.datetime] = None
227227
time_spine: Optional[TimeSpine] = None
228-
freshness: Optional[Dict[str, Any]] = None
229228

230229
def __post_init__(self) -> None:
231230
if self.latest_version:

core/dbt/parser/schemas.py

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,7 @@
1818
)
1919

2020
from dbt.artifacts.resources import RefArgs
21-
from dbt.artifacts.resources.v1.model import (
22-
CustomGranularity,
23-
ModelFreshness,
24-
TimeSpine,
25-
merge_model_freshness,
26-
)
21+
from dbt.artifacts.resources.v1.model import CustomGranularity, TimeSpine
2722
from dbt.clients.checked_load import (
2823
checked_load,
2924
issue_deprecation_warnings_for_failures,
@@ -766,8 +761,6 @@ def parse_patch(self, block: TargetBlock[NodeTarget], refs: ParserRef) -> None:
766761
deprecation_date: Optional[datetime.datetime] = None
767762
time_spine: Optional[TimeSpine] = None
768763

769-
freshness: Optional[ModelFreshness] = None
770-
771764
if isinstance(block.target, UnparsedModelUpdate):
772765
deprecation_date = block.target.deprecation_date
773766
time_spine = (
@@ -785,37 +778,6 @@ def parse_patch(self, block: TargetBlock[NodeTarget], refs: ParserRef) -> None:
785778
else None
786779
)
787780

788-
try:
789-
project_freshness_dict = self.project.models.get("+freshness", None)
790-
project_freshness = (
791-
ModelFreshness.from_dict(project_freshness_dict)
792-
if project_freshness_dict and "build_after" in project_freshness_dict
793-
else None
794-
)
795-
except ValueError:
796-
fire_event(
797-
Note(
798-
msg="Could not validate `freshness` for `models` in 'dbt_project.yml', ignoring.",
799-
),
800-
EventLevel.WARN,
801-
)
802-
project_freshness = None
803-
804-
model_freshness_dict = block.target.freshness or None
805-
model_freshness = (
806-
ModelFreshness.from_dict(model_freshness_dict)
807-
if model_freshness_dict and "build_after" in model_freshness_dict
808-
else None
809-
)
810-
811-
config_freshness_dict = block.target.config.get("freshness", None)
812-
config_freshness = (
813-
ModelFreshness.from_dict(config_freshness_dict)
814-
if config_freshness_dict and "build_after" in config_freshness_dict
815-
else None
816-
)
817-
freshness = merge_model_freshness(project_freshness, model_freshness, config_freshness)
818-
819781
patch = ParsedNodePatch(
820782
name=block.target.name,
821783
original_file_path=block.target.original_file_path,
@@ -832,7 +794,6 @@ def parse_patch(self, block: TargetBlock[NodeTarget], refs: ParserRef) -> None:
832794
constraints=block.target.constraints,
833795
deprecation_date=deprecation_date,
834796
time_spine=time_spine,
835-
freshness=freshness,
836797
)
837798
assert isinstance(self.yaml.file, SchemaSourceFile)
838799
source_file: SchemaSourceFile = self.yaml.file
@@ -1122,7 +1083,6 @@ def patch_node_properties(self, node, patch: "ParsedNodePatch") -> None:
11221083
# These two will have to be reapplied after config is built for versioned models
11231084
self.patch_constraints(node, patch.constraints)
11241085
self.patch_time_spine(node, patch.time_spine)
1125-
node.freshness = patch.freshness
11261086
node.build_contract_checksum()
11271087

11281088
def patch_constraints(self, node: ModelNode, constraints: List[Dict[str, Any]]) -> None:

tests/functional/artifacts/expected_manifest.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,6 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False):
383383
"version": None,
384384
"latest_version": None,
385385
"time_spine": None,
386-
"freshness": None,
387386
"doc_blocks": [],
388387
},
389388
"model.test.second_model": {
@@ -494,7 +493,6 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False):
494493
"version": None,
495494
"latest_version": None,
496495
"time_spine": None,
497-
"freshness": None,
498496
"doc_blocks": [],
499497
},
500498
"seed.test.seed": {
@@ -1055,7 +1053,6 @@ def expected_references_manifest(project):
10551053
"latest_version": None,
10561054
"constraints": [],
10571055
"time_spine": None,
1058-
"freshness": None,
10591056
"doc_blocks": [],
10601057
},
10611058
"model.test.ephemeral_summary": {
@@ -1133,7 +1130,6 @@ def expected_references_manifest(project):
11331130
"latest_version": None,
11341131
"constraints": [],
11351132
"time_spine": None,
1136-
"freshness": None,
11371133
"doc_blocks": ["doc.test.ephemeral_summary"],
11381134
},
11391135
"model.test.view_summary": {
@@ -1207,7 +1203,6 @@ def expected_references_manifest(project):
12071203
"latest_version": None,
12081204
"constraints": [],
12091205
"time_spine": None,
1210-
"freshness": None,
12111206
"doc_blocks": ["doc.test.view_summary"],
12121207
},
12131208
"seed.test.seed": {
@@ -1709,7 +1704,6 @@ def expected_versions_manifest(project):
17091704
"version": 1,
17101705
"latest_version": 2,
17111706
"time_spine": None,
1712-
"freshness": None,
17131707
"doc_blocks": [],
17141708
},
17151709
"model.test.versioned_model.v2": {
@@ -1792,7 +1786,6 @@ def expected_versions_manifest(project):
17921786
"version": 2,
17931787
"latest_version": 2,
17941788
"time_spine": None,
1795-
"freshness": None,
17961789
"doc_blocks": [],
17971790
},
17981791
"model.test.ref_versioned_model": {
@@ -1852,7 +1845,6 @@ def expected_versions_manifest(project):
18521845
"version": None,
18531846
"latest_version": None,
18541847
"time_spine": None,
1855-
"freshness": None,
18561848
"doc_blocks": [],
18571849
},
18581850
"test.test.unique_versioned_model_v1_first_name.6138195dec": {

tests/functional/model_config/test_freshness_config.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
- name: my_source
2323
database: "{{ target.database }}"
2424
schema: "{{ target.schema }}"
25-
freshness:
26-
warn_after: {count: 24, period: hour}
27-
error_after: {count: 48, period: hour}
25+
config:
26+
freshness:
27+
warn_after: {count: 24, period: hour}
28+
error_after: {count: 48, period: hour}
2829
loaded_at_field: _loaded_at
2930
tables:
3031
- name: source_table
@@ -35,16 +36,18 @@
3536
description: Model with no freshness defined
3637
- name: model_b
3738
description: Model with only model freshness defined
38-
freshness:
39-
build_after:
40-
count: 1
41-
period: day
42-
updates_on: all
39+
config:
40+
freshness:
41+
build_after:
42+
count: 1
43+
period: day
44+
updates_on: all
4345
- name: model_c
4446
description: Model with only source freshness defined
45-
freshness:
46-
warn_after: {count: 24, period: hour}
47-
error_after: {count: 48, period: hour}
47+
config:
48+
freshness:
49+
warn_after: {count: 24, period: hour}
50+
error_after: {count: 48, period: hour}
4851
loaded_at_field: _loaded_at
4952
tables:
5053
- name: source_table

tests/unit/contracts/graph/test_manifest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@
9898
"defer_relation",
9999
"time_spine",
100100
"batch",
101-
"freshness",
102101
}
103102
)
104103

tests/unit/parser/test_parser.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -309,22 +309,29 @@ def assertEqualNodes(node_one, node_two):
309309
models:
310310
- name: my_model
311311
description: A description of my model
312-
freshness:
313-
build_after: {count: 4, period: day, updates_on: all}
314312
config:
315313
freshness:
316-
build_after: {count: 1, period: day, updates_on: any}
314+
build_after: {count: 1, period: day, updates_on: all}
317315
"""
318316

319-
SINGLE_TABLE_MODEL_FRESHNESS_ONLY_DEPEND_ON = """
317+
SINGLE_TABLE_MODEL_TOP_LEVEL_FRESHNESS = """
320318
models:
321319
- name: my_model
322320
description: A description of my model
323321
freshness:
324-
build_after:
322+
build_after: {count: 1, period: day, updates_on: any}
323+
"""
324+
325+
SINGLE_TABLE_MODEL_FRESHNESS_ONLY_DEPEND_ON = """
326+
models:
327+
- name: my_model
328+
description: A description of my model
329+
config:
330+
freshness:
331+
build_after:
325332
updates_on: all
326333
period: hour
327-
count: 0
334+
count: 5
328335
"""
329336

330337

@@ -738,7 +745,7 @@ def setUp(self):
738745
my_model_node = MockNode(
739746
package="root",
740747
name="my_model",
741-
config=mock.MagicMock(enabled=True),
748+
config=ModelConfig(enabled=True),
742749
refs=[],
743750
sources=[],
744751
patch_path=None,
@@ -771,10 +778,23 @@ def test__parse_model_freshness(self):
771778

772779
assert self.parser.manifest.nodes[
773780
"model.root.my_model"
774-
].freshness.build_after == ModelBuildAfter(
781+
].config.freshness.build_after == ModelBuildAfter(
775782
count=1, period="day", updates_on=ModelFreshnessUpdatesOnOptions.all
776783
)
777784

785+
def test__parse_model_ignores_top_level_freshness(self):
786+
block = self.file_block_for(SINGLE_TABLE_MODEL_TOP_LEVEL_FRESHNESS, "test_one.yml")
787+
self.parser.manifest.files[block.file.file_id] = block.file
788+
dct = yaml_from_file(block.file, validate=True)
789+
self.parser.parse_file(block, dct)
790+
self.assert_has_manifest_lengths(self.parser.manifest, nodes=1)
791+
792+
# we can't use hasattr because the model node is a mock, and checking with hasattr will add it to the mock
793+
assert "freshness" not in self.parser.manifest.nodes["model.root.my_model"].__dir__()
794+
795+
# should be None because nothing set it
796+
assert self.parser.manifest.nodes["model.root.my_model"].config.freshness is None
797+
778798
def test__parse_model_freshness_depend_on(self):
779799
block = self.file_block_for(SINGLE_TABLE_MODEL_FRESHNESS_ONLY_DEPEND_ON, "test_one.yml")
780800
self.parser.manifest.files[block.file.file_id] = block.file
@@ -783,8 +803,8 @@ def test__parse_model_freshness_depend_on(self):
783803
self.assert_has_manifest_lengths(self.parser.manifest, nodes=1)
784804
assert self.parser.manifest.nodes[
785805
"model.root.my_model"
786-
].freshness.build_after == ModelBuildAfter(
787-
count=0, period="hour", updates_on=ModelFreshnessUpdatesOnOptions.all
806+
].config.freshness.build_after == ModelBuildAfter(
807+
count=5, period="hour", updates_on=ModelFreshnessUpdatesOnOptions.all
788808
)
789809

790810
def test__read_basic_model_tests_wrong_severity(self):

0 commit comments

Comments
 (0)