Skip to content

Commit 740cbc2

Browse files
authored
[dagster-dbt] Fix issue with non-portable partial-parse (#33074)
## Summary & Motivation Kinda a gnarly fix but the quick explanation here is that for seeds specifically, dbt serializes information about the absolute path to a given seed file inside the partial_parse.msgpack file that gets created when you run `dbt parse`. This information gets used when dbt attempts to load that seed file in a run, meaning if you generated the manifest in a different system than the one where you execute the command, seed execution can fail because the path will be incorrect. This strips out the seed information from the partial parse file, which basically forces dbt to recalculate that information every time a dbt command runs. This is thankfully very fast for seeds (it's just a reference to the seed files), and so doesn't introduce latency in the way that doing this for models would. ## How I Tested These Changes ## Changelog > Insert changelog entry or delete this section.
1 parent 69df437 commit 740cbc2

2 files changed

Lines changed: 118 additions & 1 deletion

File tree

python_modules/libraries/dagster-dbt/dagster_dbt/dbt_project.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,42 @@ def _prepare_manifest(self, project: "DbtProject") -> None:
132132
.wait()
133133
)
134134

135+
# Remove seed entries from partial_parse to force re-parsing at runtime.
136+
# This ensures seeds get correct root_path based on current project location.
137+
self._invalidate_seeds_in_partial_parse(project)
138+
139+
def _invalidate_seeds_in_partial_parse(self, project: "DbtProject") -> None:
140+
"""Remove seed entries from partial_parse.msgpack to force re-parsing.
141+
142+
Seeds contain root_path which is an absolute path from build time. When state
143+
is generated in one environment (e.g., CI/CD) and used in another (e.g., deployed
144+
container), the root_path points to the wrong location and seed loading fails.
145+
146+
By removing seed entries from the cache, dbt will re-parse them at runtime with
147+
the correct current project path. Models keep their cached data for fast loading.
148+
"""
149+
import msgpack
150+
151+
partial_parse_path = project.project_dir / project.target_path / "partial_parse.msgpack"
152+
if not partial_parse_path.exists():
153+
return
154+
155+
with open(partial_parse_path, "rb") as f:
156+
data = msgpack.unpack(f, raw=False, strict_map_key=False)
157+
158+
# Remove seed nodes
159+
seed_node_ids = [k for k in data.get("nodes", {}).keys() if k.startswith("seed.")]
160+
for seed_id in seed_node_ids:
161+
del data["nodes"][seed_id]
162+
163+
# Remove seed file entries (CSVs)
164+
seed_file_ids = [k for k in data.get("files", {}).keys() if k.lower().endswith(".csv")]
165+
for file_id in seed_file_ids:
166+
del data["files"][file_id]
167+
168+
with open(partial_parse_path, "wb") as f:
169+
msgpack.pack(data, f)
170+
135171

136172
@record_custom
137173
class DbtProject(IHaveNew):

python_modules/libraries/dagster-dbt/dagster_dbt_tests/core/test_project.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import multiprocessing
22

3+
import msgpack
34
import pytest
45
from dagster._core.test_utils import environ
56
from dagster._utils.test import copy_directory
67
from dagster_dbt.dbt_manifest import validate_manifest
7-
from dagster_dbt.dbt_project import DbtProject
8+
from dagster_dbt.dbt_project import DagsterDbtProjectPreparer, DbtProject
89

910
from dagster_dbt_tests.dbt_projects import test_jaffle_shop_path
1011

@@ -53,3 +54,83 @@ def test_concurrent_processes(project_dir):
5354
assert proc.exitcode == 0
5455

5556
assert my_project.manifest_path.exists()
57+
58+
59+
def test_invalidate_seeds_in_partial_parse(project_dir) -> None:
60+
"""Test that seed entries are removed from partial_parse.msgpack after preparation.
61+
62+
Seeds contain root_path which is an absolute path from build time. When state
63+
is generated in one environment (CI/CD) and used in another (deployed container),
64+
the root_path points to the wrong location and seed loading fails.
65+
66+
By removing seed entries from the cache, dbt will re-parse them at runtime with
67+
the correct current project path. Models keep their cached data for fast loading.
68+
"""
69+
my_project = DbtProject(project_dir)
70+
partial_parse_path = my_project.project_dir / my_project.target_path / "partial_parse.msgpack"
71+
72+
# Prepare the project which generates partial_parse.msgpack
73+
with environ({"DAGSTER_IS_DEV_CLI": "1"}):
74+
my_project.prepare_if_dev()
75+
76+
assert partial_parse_path.exists(), "partial_parse.msgpack should exist after preparation"
77+
78+
# Read the partial_parse and verify seeds were removed
79+
with open(partial_parse_path, "rb") as f:
80+
data = msgpack.unpack(f, raw=False, strict_map_key=False)
81+
82+
# Check that no seed nodes remain
83+
seed_node_ids = [k for k in data.get("nodes", {}).keys() if k.startswith("seed.")]
84+
assert len(seed_node_ids) == 0, f"Expected no seed nodes, but found: {seed_node_ids}"
85+
86+
# Check that no seed files remain (CSVs)
87+
seed_file_ids = [k for k in data.get("files", {}).keys() if k.lower().endswith(".csv")]
88+
assert len(seed_file_ids) == 0, f"Expected no seed files, but found: {seed_file_ids}"
89+
90+
# Check that model nodes are preserved
91+
model_node_ids = [k for k in data.get("nodes", {}).keys() if k.startswith("model.")]
92+
assert len(model_node_ids) > 0, "Model nodes should be preserved"
93+
94+
# Check that model files are preserved
95+
model_file_ids = [k for k in data.get("files", {}).keys() if ".sql" in k.lower()]
96+
assert len(model_file_ids) > 0, "Model files should be preserved"
97+
98+
99+
def test_invalidate_seeds_handles_missing_partial_parse() -> None:
100+
"""Test that _invalidate_seeds_in_partial_parse handles missing file gracefully."""
101+
with copy_directory(test_jaffle_shop_path) as project_dir:
102+
my_project = DbtProject(project_dir)
103+
partial_parse_path = (
104+
my_project.project_dir / my_project.target_path / "partial_parse.msgpack"
105+
)
106+
107+
# Ensure partial_parse doesn't exist
108+
if partial_parse_path.exists():
109+
partial_parse_path.unlink()
110+
111+
# Should not raise an error
112+
preparer = DagsterDbtProjectPreparer()
113+
preparer._invalidate_seeds_in_partial_parse(my_project) # noqa: SLF001
114+
115+
116+
def test_seed_command_succeeds_after_invalidation() -> None:
117+
"""Test that dbt seed command works after seed entries are invalidated.
118+
119+
This is the end-to-end validation that removing seed entries from
120+
partial_parse.msgpack allows dbt to correctly re-parse and load seeds.
121+
"""
122+
from dagster_dbt import DbtCliResource
123+
124+
with copy_directory(test_jaffle_shop_path) as project_dir:
125+
my_project = DbtProject(project_dir)
126+
127+
# Prepare the project (which invalidates seeds in partial_parse)
128+
with environ({"DAGSTER_IS_DEV_CLI": "1"}):
129+
my_project.prepare_if_dev()
130+
131+
# Run dbt seed - this should succeed because dbt will re-parse
132+
# the seed files with correct paths
133+
dbt = DbtCliResource(project_dir=my_project)
134+
result = dbt.cli(["seed"]).wait()
135+
136+
assert result.is_successful(), "dbt seed failed"

0 commit comments

Comments
 (0)