Skip to content

Commit d3cb077

Browse files
Enforce dimensions have 'validity_params' only if they have granularity (#12473)
* Enforce dimensions have 'validity_params' only if they have granularity * Add enforcement for derived dimensions, too * Add changie file.
1 parent 0d03e2a commit d3cb077

File tree

3 files changed

+110
-1
lines changed

3 files changed

+110
-1
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: Provide user-friendly validations that dimensions with 'validity_params' also have granularities.
3+
time: 2026-02-10T15:40:42.808774-08:00
4+
custom:
5+
Author: theyostalservice
6+
Issue: "12473"

core/dbt/contracts/graph/unparsed.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,18 @@ class UnparsedDerivedDimensionV2(UnparsedDimensionV2):
188188
expr: str
189189
granularity: Optional[str] = None # str is really a TimeGranularity Enum
190190

191+
@classmethod
192+
@override
193+
def validate(cls, data):
194+
super().validate(data)
195+
# validity_params may only be set when the derived dimension has a granularity
196+
if data.get("validity_params") is not None and not data.get("granularity"):
197+
dim_name = data.get("name")
198+
raise ValidationError(
199+
f"Derived dimension {dim_name} has validity_params, "
200+
"so it must specify a granularity."
201+
)
202+
191203

192204
@dataclass
193205
class UnparsedEntityBase(dbtClassMixin):
@@ -253,6 +265,18 @@ def validate(cls, data):
253265
f"column {data.get('name')}, "
254266
"so that column must specify a granularity."
255267
)
268+
# validity_params may only be set when the column has a granularity
269+
if (
270+
isinstance(dimension, dict)
271+
and dimension.get("validity_params") is not None
272+
and not data.get("granularity")
273+
):
274+
dim_name = dimension.get("name") or data.get("name")
275+
raise ValidationError(
276+
f"Dimension {dim_name} has validity_params attached to "
277+
f"column {data.get('name')}, "
278+
"so that column must specify a granularity."
279+
)
256280

257281

258282
@dataclass

tests/unit/contracts/graph/test_unparsed.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
HasColumnTests,
2222
UnparsedColumn,
2323
UnparsedConversionTypeParams,
24+
UnparsedDerivedDimensionV2,
2425
UnparsedDocumentationFile,
2526
UnparsedExposure,
2627
UnparsedMacro,
@@ -1140,7 +1141,8 @@ def test_column_parse():
11401141

11411142

11421143
class TestUnparsedColumnTimeDimensionGranularityValidation(ContractTestCase):
1143-
"""Test validation that SL YAML V2 column with time dimension must specify granularity."""
1144+
"""Test validation that SL YAML V2 column with time dimension must specify granularity,
1145+
and that dimension validity_params require granularity."""
11441146

11451147
ContractType = UnparsedColumn
11461148

@@ -1189,3 +1191,80 @@ def test_non_time_dimension_string_passes_without_granularity(self):
11891191
col = self.ContractType.from_dict(column_dict)
11901192
self.assertEqual(col.name, "category")
11911193
self.assertIsNone(col.granularity)
1194+
1195+
def test_dimension_with_validity_params_without_granularity_fails_validation(self):
1196+
"""Dimension (dict) with validity_params must have column granularity."""
1197+
column_dict = {
1198+
"name": "valid_from",
1199+
"dimension": {
1200+
"type": "time",
1201+
"name": "valid_from_dim",
1202+
"validity_params": {"is_start": True, "is_end": False},
1203+
},
1204+
}
1205+
self.assert_fails_validation(column_dict)
1206+
1207+
def test_dimension_with_validity_params_with_granularity_passes_validation(self):
1208+
"""Dimension (dict) with validity_params and granularity passes."""
1209+
column_dict = {
1210+
"name": "valid_from",
1211+
"granularity": "day",
1212+
"dimension": {
1213+
"type": "time",
1214+
"name": "valid_from_dim",
1215+
"validity_params": {"is_start": True, "is_end": True},
1216+
},
1217+
}
1218+
col = self.ContractType.from_dict(column_dict)
1219+
self.assertEqual(col.granularity, "day")
1220+
self.assertEqual(col.name, "valid_from")
1221+
1222+
def test_dimension_without_validity_params_passes_without_granularity_when_not_time(self):
1223+
"""Dimension (dict) without validity_params and not time does not require granularity."""
1224+
column_dict = {
1225+
"name": "category",
1226+
"dimension": {"type": "categorical", "name": "category_dim"},
1227+
}
1228+
col = self.ContractType.from_dict(column_dict)
1229+
self.assertEqual(col.name, "category")
1230+
self.assertIsNone(col.granularity)
1231+
1232+
1233+
class TestUnparsedDerivedDimensionV2ValidityParamsValidation(ContractTestCase):
1234+
"""Test validation that UnparsedDerivedDimensionV2 only has validity_params when it has granularity."""
1235+
1236+
ContractType = UnparsedDerivedDimensionV2
1237+
1238+
def test_derived_dimension_with_validity_params_without_granularity_fails_validation(self):
1239+
"""Derived dimension with validity_params must have granularity."""
1240+
dimension_dict = {
1241+
"type": "time",
1242+
"name": "valid_from_dim",
1243+
"expr": "valid_from",
1244+
"validity_params": {"is_start": True, "is_end": False},
1245+
}
1246+
self.assert_fails_validation(dimension_dict)
1247+
1248+
def test_derived_dimension_with_validity_params_with_granularity_passes_validation(self):
1249+
"""Derived dimension with validity_params and granularity passes."""
1250+
dimension_dict = {
1251+
"type": "time",
1252+
"name": "valid_from_dim",
1253+
"expr": "valid_from",
1254+
"granularity": "day",
1255+
"validity_params": {"is_start": True, "is_end": True},
1256+
}
1257+
dim = self.ContractType.from_dict(dimension_dict)
1258+
self.assertEqual(dim.granularity, "day")
1259+
self.assertEqual(dim.name, "valid_from_dim")
1260+
1261+
def test_derived_dimension_without_validity_params_passes_without_granularity(self):
1262+
"""Derived dimension without validity_params does not require granularity."""
1263+
dimension_dict = {
1264+
"type": "time",
1265+
"name": "some_dim",
1266+
"expr": "some_column",
1267+
}
1268+
dim = self.ContractType.from_dict(dimension_dict)
1269+
self.assertEqual(dim.name, "some_dim")
1270+
self.assertIsNone(dim.granularity)

0 commit comments

Comments
 (0)