Skip to content

Commit ab70889

Browse files
fix: [AI-7675] accept reused/unknown run statuses in run_results v1-v5 parsers (#108)
Mirror of the v6 `Status` shim (AI-7435, PR #106) for the pre-v6 schemas, closing the residual of AI-7675 finding #2: any run status outside `success`/`error`/`skipped` (e.g. dbt 2.0 `reused`, or any future status) raised a `ValidationError` in run_results v1-v5 and the ingestion worker silently dropped the entire `run_results.json`. - Make `Status` a `str, Enum`, add `reused`, and add the `_missing_` forward-compat fallback so unknown statuses surface as real members instead of failing validation (identical to the v6 fix). - Reorder `RunResultOutput.status` to `Union[Status1, Status2, Status]` so test/freshness statuses (`pass`/`fail`/`warn`/`runtime error`) keep resolving via the strict enums and the permissive run `Status` is tried last (same reordering as v6). - Add parameterized regression tests over v1-v5 mirroring `test_run_results_v6.py`, including full-file `parse_run_results`. Freshness-specific enums (`Status3`/`Status4` in v3/v4) are untouched, matching the v6 fix scope. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 4c2353d commit ab70889

6 files changed

Lines changed: 167 additions & 10 deletions

File tree

src/vendor/dbt_artifacts_parser/parsers/run_results/run_results_v1.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,22 @@ class BaseArtifactMetadata(BaseParserModel):
2525
env: Optional[dict[str, str]] = {}
2626

2727

28-
class Status(Enum):
28+
class Status(str, Enum):
2929
success = "success"
3030
error = "error"
3131
skipped = "skipped"
32+
reused = "reused"
33+
34+
@classmethod
35+
def _missing_(cls, value):
36+
# Forward-compatibility: dbt periodically introduces new run statuses
37+
# (e.g. "reused" in dbt 2.0). Surface unknown values as real members so
38+
# downstream `.value` access keeps working instead of failing validation
39+
# and silently dropping the entire run_results.json.
40+
member = str.__new__(cls, value)
41+
member._name_ = str(value)
42+
member._value_ = value
43+
return member
3244

3345

3446
class Status1(Enum):
@@ -58,7 +70,7 @@ class RunResultOutput(BaseParserModel):
5870
model_config = ConfigDict(
5971
extra="allow",
6072
)
61-
status: Union[Status, Status1, Status2]
73+
status: Union[Status1, Status2, Status]
6274
timing: list[TimingInfo]
6375
thread_id: str
6476
execution_time: float

src/vendor/dbt_artifacts_parser/parsers/run_results/run_results_v2.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,22 @@ class BaseArtifactMetadata(BaseParserModel):
2525
env: Optional[dict[str, str]] = {}
2626

2727

28-
class Status(Enum):
28+
class Status(str, Enum):
2929
success = "success"
3030
error = "error"
3131
skipped = "skipped"
32+
reused = "reused"
33+
34+
@classmethod
35+
def _missing_(cls, value):
36+
# Forward-compatibility: dbt periodically introduces new run statuses
37+
# (e.g. "reused" in dbt 2.0). Surface unknown values as real members so
38+
# downstream `.value` access keeps working instead of failing validation
39+
# and silently dropping the entire run_results.json.
40+
member = str.__new__(cls, value)
41+
member._name_ = str(value)
42+
member._value_ = value
43+
return member
3244

3345

3446
class Status1(Enum):
@@ -58,7 +70,7 @@ class RunResultOutput(BaseParserModel):
5870
model_config = ConfigDict(
5971
extra="allow",
6072
)
61-
status: Union[Status, Status1, Status2]
73+
status: Union[Status1, Status2, Status]
6274
timing: list[TimingInfo]
6375
thread_id: str
6476
execution_time: float

src/vendor/dbt_artifacts_parser/parsers/run_results/run_results_v3.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,22 @@ class BaseArtifactMetadata(BaseParserModel):
2525
env: Optional[dict[str, str]] = {}
2626

2727

28-
class Status(Enum):
28+
class Status(str, Enum):
2929
success = "success"
3030
error = "error"
3131
skipped = "skipped"
32+
reused = "reused"
33+
34+
@classmethod
35+
def _missing_(cls, value):
36+
# Forward-compatibility: dbt periodically introduces new run statuses
37+
# (e.g. "reused" in dbt 2.0). Surface unknown values as real members so
38+
# downstream `.value` access keeps working instead of failing validation
39+
# and silently dropping the entire run_results.json.
40+
member = str.__new__(cls, value)
41+
member._name_ = str(value)
42+
member._value_ = value
43+
return member
3244

3345

3446
class Status1(Enum):
@@ -104,7 +116,7 @@ class RunResultOutput(BaseParserModel):
104116
model_config = ConfigDict(
105117
extra="allow",
106118
)
107-
status: Union[Status, Status1, Status2]
119+
status: Union[Status1, Status2, Status]
108120
timing: list[TimingInfo]
109121
thread_id: str
110122
execution_time: float

src/vendor/dbt_artifacts_parser/parsers/run_results/run_results_v4.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,22 @@ class BaseArtifactMetadata(BaseParserModel):
2525
env: Optional[dict[str, str]] = {}
2626

2727

28-
class Status(Enum):
28+
class Status(str, Enum):
2929
success = "success"
3030
error = "error"
3131
skipped = "skipped"
32+
reused = "reused"
33+
34+
@classmethod
35+
def _missing_(cls, value):
36+
# Forward-compatibility: dbt periodically introduces new run statuses
37+
# (e.g. "reused" in dbt 2.0). Surface unknown values as real members so
38+
# downstream `.value` access keeps working instead of failing validation
39+
# and silently dropping the entire run_results.json.
40+
member = str.__new__(cls, value)
41+
member._name_ = str(value)
42+
member._value_ = value
43+
return member
3244

3345

3446
class Status1(Enum):
@@ -104,7 +116,7 @@ class RunResultOutput(BaseParserModel):
104116
model_config = ConfigDict(
105117
extra="allow",
106118
)
107-
status: Union[Status, Status1, Status2]
119+
status: Union[Status1, Status2, Status]
108120
timing: list[TimingInfo]
109121
thread_id: str
110122
execution_time: float

src/vendor/dbt_artifacts_parser/parsers/run_results/run_results_v5.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,22 @@ class TimingInfo(BaseParserModel):
3333
completed_at: Optional[str] = None
3434

3535

36-
class Status(Enum):
36+
class Status(str, Enum):
3737
success = "success"
3838
error = "error"
3939
skipped = "skipped"
40+
reused = "reused"
41+
42+
@classmethod
43+
def _missing_(cls, value):
44+
# Forward-compatibility: dbt periodically introduces new run statuses
45+
# (e.g. "reused" in dbt 2.0). Surface unknown values as real members so
46+
# downstream `.value` access keeps working instead of failing validation
47+
# and silently dropping the entire run_results.json.
48+
member = str.__new__(cls, value)
49+
member._name_ = str(value)
50+
member._value_ = value
51+
return member
4052

4153

4254
class Status1(Enum):
@@ -58,7 +70,7 @@ class RunResultOutput(BaseParserModel):
5870
model_config = ConfigDict(
5971
extra="allow",
6072
)
61-
status: Union[Status, Status1, Status2]
73+
status: Union[Status1, Status2, Status]
6274
timing: list[TimingInfo]
6375
thread_id: str
6476
execution_time: float
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Tests for run_results v1-v5 parsers, specifically the resilient run `Status` enum.
2+
3+
Mirrors ``test_run_results_v6.py``: the v6 `Status` shim (AI-7435) was never
4+
applied to the pre-v6 schemas, so a `reused` (or any future unknown) run status
5+
raised a ``ValidationError`` and the entire run_results.json was silently
6+
dropped during ingestion (AI-7675 finding #2 residual).
7+
"""
8+
import importlib
9+
10+
import pytest
11+
12+
from vendor.dbt_artifacts_parser.parser import parse_run_results
13+
14+
VERSIONS = [1, 2, 3, 4, 5]
15+
16+
17+
def _module(version: int):
18+
return importlib.import_module(
19+
f"vendor.dbt_artifacts_parser.parsers.run_results.run_results_v{version}"
20+
)
21+
22+
23+
def _result(status: str, unique_id: str) -> dict:
24+
return {
25+
"status": status,
26+
"timing": [],
27+
"thread_id": "Thread-1",
28+
"execution_time": 0.1,
29+
"adapter_response": {},
30+
"unique_id": unique_id,
31+
}
32+
33+
34+
def _run_results(version: int, *statuses: str) -> dict:
35+
return {
36+
"metadata": {
37+
"dbt_schema_version": f"https://schemas.getdbt.com/dbt/run-results/v{version}.json",
38+
"dbt_version": "1.5.0",
39+
"invocation_id": "test-invocation-123",
40+
},
41+
"elapsed_time": 1.5,
42+
"args": {},
43+
"results": [_result(s, f"model.proj.m{i}") for i, s in enumerate(statuses)],
44+
}
45+
46+
47+
@pytest.mark.parametrize("version", VERSIONS)
48+
class TestRunResultStatusPreV6:
49+
"""The run `Status` enum must accept new/unknown dbt statuses without failing."""
50+
51+
def test_reused_status_parses(self, version):
52+
mod = _module(version)
53+
result = mod.RunResultOutput(**_result("reused", "model.proj.a"))
54+
assert result.status.value == "reused"
55+
assert result.status is mod.Status.reused
56+
57+
def test_known_statuses_preserve_value(self, version):
58+
mod = _module(version)
59+
for status in ("success", "error", "skipped"):
60+
assert (
61+
mod.RunResultOutput(**_result(status, "model.proj.a")).status.value
62+
== status
63+
)
64+
65+
def test_unknown_future_status_parses(self, version):
66+
mod = _module(version)
67+
result = mod.RunResultOutput(**_result("some_future_status", "model.proj.a"))
68+
assert result.status.value == "some_future_status"
69+
70+
def test_test_and_freshness_statuses_keep_their_value(self, version):
71+
"""Test/freshness statuses must keep their `.value` (resolved via Status1/Status2)."""
72+
mod = _module(version)
73+
for status in ("pass", "fail", "warn", "runtime error"):
74+
assert (
75+
mod.RunResultOutput(**_result(status, "test.proj.t")).status.value
76+
== status
77+
)
78+
79+
80+
@pytest.mark.parametrize("version", VERSIONS)
81+
class TestParseRunResultsEntryPointPreV6:
82+
"""The public `parse_run_results` must parse a full file containing `reused`."""
83+
84+
def test_file_with_reused_result_parses_fully(self, version):
85+
run_results = parse_run_results(
86+
_run_results(version, "success", "reused", "skipped", "pass", "error", "no-op")
87+
)
88+
# Previously this whole file was dropped because of the single `reused` row.
89+
assert len(run_results.results) == 6
90+
assert {r.status.value for r in run_results.results} == {
91+
"success",
92+
"reused",
93+
"skipped",
94+
"pass",
95+
"error",
96+
"no-op",
97+
}

0 commit comments

Comments
 (0)