Skip to content

Commit f8820ff

Browse files
Adding children in addition to parents (#113)
As discussed [here](#111), adding `children` to models / sources / snapshots. This is a simple addition to the logic that populates parents; basically, if `x` is one of `y`'s parents, then `y` is one of `x`'s children. This allows writing rules like: ``` @rule(severity=Severity.CRITICAL) def children_of_beta_models_should_be_beta_models( model: Model, ) -> RuleViolation | None: """Children of beta models should be beta models""" if "beta" in model.tags: for child in model.children: if "beta" not in child.tags: return RuleViolation( message=f"Beta model {model.name} has a child that isn't a beta model {child.name}" ) return None ``` This will need to be updated slightly for new evaluable types, but that should be easy. --------- Co-authored-by: Jochem van Dooren <[email protected]>
1 parent cb57712 commit f8820ff

File tree

4 files changed

+39
-8
lines changed

4 files changed

+39
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to
99
## [Unreleased]
1010

1111
- Add support for linting and scoring dbt seeds (#110)
12+
- Add `children` to models, snapshots, and sources. (#113)
1213
- Add `parents` to models and snapshots, allowing access to parent nodes. (#109)
1314

1415
## [0.11.0] - 2025-04-04

src/dbt_score/models.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections import defaultdict
77
from dataclasses import dataclass, field
88
from pathlib import Path
9-
from typing import Any, Iterable, List, Literal, TypeAlias, Union
9+
from typing import Any, Iterable, Literal, TypeAlias, Union
1010

1111
from dbt_score.dbt_utils import dbt_ls
1212

@@ -156,6 +156,7 @@ def _get_columns(
156156

157157
# Type annotation for parent references
158158
ParentType = Union["Model", "Source", "Snapshot", "Seed"]
159+
ChildType = Union["Model", "Snapshot"]
159160

160161

161162
@dataclass
@@ -184,6 +185,7 @@ class Model(HasColumnsMixin):
184185
tests: The list of tests attached to the model.
185186
depends_on: Dictionary of models/sources/macros that the model depends on.
186187
parents: The list of models, sources, and snapshots this model depends on.
188+
children: The list of models and snapshots that depend on this model.
187189
_raw_values: The raw values of the model (node) in the manifest.
188190
_raw_test_values: The raw test values of the model (node) in the manifest.
189191
"""
@@ -209,7 +211,8 @@ class Model(HasColumnsMixin):
209211
tests: list[Test] = field(default_factory=list)
210212
depends_on: dict[str, list[str]] = field(default_factory=dict)
211213
constraints: list[Constraint] = field(default_factory=list)
212-
parents: List[ParentType] = field(default_factory=list)
214+
parents: list[ParentType] = field(default_factory=list)
215+
children: list[ChildType] = field(default_factory=list)
213216
_raw_values: dict[str, Any] = field(default_factory=dict)
214217
_raw_test_values: list[dict[str, Any]] = field(default_factory=list)
215218

@@ -319,6 +322,7 @@ class Source(HasColumnsMixin):
319322
patch_path: The yml path of the source definition.
320323
tags: The list of tags attached to the source table.
321324
tests: The list of tests attached to the source table.
325+
children: The list of models and snapshots that depend on this source.
322326
_raw_values: The raw values of the source definition in the manifest.
323327
_raw_test_values: The raw test values of the source definition in the manifest.
324328
"""
@@ -342,6 +346,7 @@ class Source(HasColumnsMixin):
342346
patch_path: str | None = None
343347
tags: list[str] = field(default_factory=list)
344348
tests: list[Test] = field(default_factory=list)
349+
children: list[ChildType] = field(default_factory=list)
345350
_raw_values: dict[str, Any] = field(default_factory=dict)
346351
_raw_test_values: list[dict[str, Any]] = field(default_factory=list)
347352

@@ -424,6 +429,7 @@ class Snapshot(HasColumnsMixin):
424429
strategy: The strategy of the snapshot.
425430
unique_key: The unique key of the snapshot.
426431
parents: The list of models, sources, and snapshots this snapshot depends on.
432+
children: The list of models and snapshots that depend on this snapshot.
427433
_raw_values: The raw values of the snapshot (node) in the manifest.
428434
_raw_test_values: The raw test values of the snapshot (node) in the manifest.
429435
"""
@@ -448,7 +454,8 @@ class Snapshot(HasColumnsMixin):
448454
depends_on: dict[str, list[str]] = field(default_factory=dict)
449455
strategy: str | None = None
450456
unique_key: list[str] | None = None
451-
parents: List[ParentType] = field(default_factory=list)
457+
parents: list[ParentType] = field(default_factory=list)
458+
children: list[ChildType] = field(default_factory=list)
452459
_raw_values: dict[str, Any] = field(default_factory=dict)
453460
_raw_test_values: list[dict[str, Any]] = field(default_factory=list)
454461

@@ -512,6 +519,7 @@ class Seed(HasColumnsMixin):
512519
patch_path: The yml path of the seed, e.g. `seeds.yml`.
513520
tags: The list of tags attached to the seed.
514521
tests: The list of tests attached to the seed.
522+
children: The list of models and snapshots that depend on this seed.
515523
_raw_values: The raw values of the seed (node) in the manifest.
516524
_raw_test_values: The raw test values of the seed (node) in the manifest.
517525
"""
@@ -531,6 +539,7 @@ class Seed(HasColumnsMixin):
531539
patch_path: str | None = None
532540
tags: list[str] = field(default_factory=list)
533541
tests: list[Test] = field(default_factory=list)
542+
children: list[ChildType] = field(default_factory=list)
534543
_raw_values: dict[str, Any] = field(default_factory=dict)
535544
_raw_test_values: list[dict[str, Any]] = field(default_factory=list)
536545

@@ -607,7 +616,7 @@ def __init__(self, file_path: Path, select: Iterable[str] | None = None):
607616
self._load_sources()
608617
self._load_snapshots()
609618
self._load_seeds()
610-
self._populate_parents()
619+
self._populate_relatives()
611620

612621
if select:
613622
self._filter_evaluables(select)
@@ -661,18 +670,22 @@ def _reindex_tests(self) -> None:
661670
):
662671
self.tests[node_unique_id].append(node_values)
663672

664-
def _populate_parents(self) -> None:
665-
"""Populate `parents` for all models and snapshots."""
673+
def _populate_relatives(self) -> None:
674+
"""Populate `parents` and `children` for all evaluables."""
666675
for node in list(self.models.values()) + list(self.snapshots.values()):
667676
for parent_id in node.depends_on.get("nodes", []):
668677
if parent_id in self.models:
669678
node.parents.append(self.models[parent_id])
679+
self.models[parent_id].children.append(node)
670680
elif parent_id in self.snapshots:
671681
node.parents.append(self.snapshots[parent_id])
682+
self.snapshots[parent_id].children.append(node)
672683
elif parent_id in self.sources:
673684
node.parents.append(self.sources[parent_id])
685+
self.sources[parent_id].children.append(node)
674686
elif parent_id in self.seeds:
675687
node.parents.append(self.seeds[parent_id])
688+
self.seeds[parent_id].children.append(node)
676689

677690
def _filter_evaluables(self, select: Iterable[str]) -> None:
678691
"""Filter evaluables like dbt's --select."""

tests/resources/manifest.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@
134134
"alias": "model2_alias",
135135
"patch_path": "/path/to/model2.yml",
136136
"tags": [],
137-
"depends_on": {},
137+
"depends_on": {
138+
"nodes": ["seed.package.seed1"]
139+
},
138140
"language": "sql",
139141
"access": "public",
140142
"group": "them_over_there"

tests/test_models.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,30 @@ def test_manifest_load(mock_read_text, raw_manifest):
4040
assert loader.snapshots["snapshot.package.snapshot1"].parents == [
4141
loader.models["model.package.model1"]
4242
]
43+
assert loader.models["model.package.model1"].children == [
44+
loader.snapshots["snapshot.package.snapshot1"]
45+
]
4346
assert loader.models["model.package.model1"].parents == [
4447
loader.models["model.package.model2"],
4548
loader.sources["source.package.my_source.table1"],
4649
loader.snapshots["snapshot.package.snapshot2"],
4750
]
48-
assert loader.models["model.package.model2"].parents == []
51+
assert loader.models["model.package.model2"].children == [
52+
loader.models["model.package.model1"]
53+
]
54+
assert loader.models["model.package.model2"].parents == [
55+
loader.seeds["seed.package.seed1"]
56+
]
4957
assert loader.snapshots["snapshot.package.snapshot2"].parents == [
5058
loader.sources["source.package.my_source.table1"]
5159
]
60+
assert loader.sources["source.package.my_source.table1"].children == [
61+
loader.models["model.package.model1"],
62+
loader.snapshots["snapshot.package.snapshot2"],
63+
]
64+
assert loader.seeds["seed.package.seed1"].children == [
65+
loader.models["model.package.model2"]
66+
]
5267

5368

5469
@patch("dbt_score.models.Path.read_text")

0 commit comments

Comments
 (0)