Fixes 29824: ingest dbt results with a null message and stop compile-only stubs shadowing executed results - #31138
Fixes 29824: ingest dbt results with a null message and stop compile-only stubs shadowing executed results#31138TeddyCr wants to merge 2 commits into
Conversation
…essage PR open-metadata#26812 added a guard in add_dbt_test_result() that skipped any run_results entry whose `message` was null, to drop the compiled-only test nodes that `dbt run` writes with status="success". The guard never looked at `status`, so it also dropped genuine executed data tests: dbt only populates `message` on failure/warn, so a passing test arrives as status="pass", message=null. That is why dbt test results stopped appearing after the 1.13.0 upgrade. Move the discrimination into `is_compiled_only_result()` in dbt_utils and key it on both `status` and `message`, so only status="success" entries with no message are treated as compiled-only. `status` is safe to dereference: it is a required, non-nullable Enum on every run-results model the shared parser can produce (v1-v6 plus the two cloud variants), and all connector variants parse through DbtServiceSource.get_dbt_objects. `failures` is not usable as the discriminator because it is stripped by REQUIRED_RESULTS_KEYS before parsing. Supersedes the same narrowing proposed in PR open-metadata#29828 by ayush-shah. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…xecuted results _get_latest_result() de-duplicated a unique_id across multiple run_results.json files purely by `execute.completed_at`. A project that keeps both a `dbt test` artifact and a later `dbt docs generate` artifact therefore selected the docs run's compile-only stub (status="success", message=null), and add_dbt_test_result() then dropped it as compiled-only - so the real pass/fail result never reached OpenMetadata even with the message guard fixed. Reported on 1.13.1 / dbt 1.11 by ziggekatten. Filter compile-only entries out of the candidate set before ranking by timestamp, reusing the same is_compiled_only_result() predicate as the ingestion guard so the two can never disagree. Sharing the predicate is load-bearing rather than incidental: if the selector's notion of "compile-only" diverged from the ingester's, the selector could hand over a result the ingester then drops, which is exactly this bug. When every match is a stub the previous behaviour is kept, since there is nothing better to pick. The no-parseable-timestamp fallback now returns the first *executed* candidate rather than the first candidate overall; that behaviour change is pinned by test_executed_result_wins_when_no_timestamp_is_usable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code Review ✅ Approved 1 resolved / 1 findingsFixes dbt test ingestion by refining the null-message guard and ensuring executed results are preferred over compile-only stubs. Consider adding a safety check for status.value to prevent potential attribute errors. ✅ 1 resolved✅ Bug: _get_latest_result now dereferences status.value unguarded
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar | Powered by Gitar — free for open source |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds regression coverage and logic adjustments to correctly distinguish executed dbt test results from compiled-only stubs, ensuring OpenMetadata ingests the right test outcome even when multiple dbt artifacts exist.
Changes:
- Add helper utilities + new regression tests for issue #29824 scenarios (null message, compiled-only stubs, latest-result selection).
- Update
_get_latest_resultto prefer executed results over compiled-only entries. - Centralize compiled-only detection in a new
is_compiled_only_resultutility and use it in ingestion logic.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| ingestion/tests/unit/test_dbt.py | Adds regression tests + payload helpers to simulate real dbt artifacts and validate ingestion/selection behavior. |
| ingestion/src/metadata/ingestion/source/database/dbt/metadata.py | Updates latest-result selection and ingestion skip logic to use compiled-only detection. |
| ingestion/src/metadata/ingestion/source/database/dbt/dbt_utils.py | Introduces is_compiled_only_result helper used by selection + ingestion. |
| self.assertIs(got, new_result) | ||
|
|
||
|
|
||
| class TestGetLatestResultPrefersExecutedResults: |
| source.metadata.add_test_case_results.assert_called_once() | ||
|
|
||
|
|
||
| class TestAddDbtTestResultNullMessage: |
| return primary_table_fqn | ||
|
|
||
|
|
||
| def is_compiled_only_result(dbt_test_result) -> bool: |
| alone cannot be used as the discriminator (issue #29824). ``failures`` would be | ||
| the other signal but it is dropped by ``REQUIRED_RESULTS_KEYS`` before parsing. | ||
| """ | ||
| return not dbt_test_result.message and dbt_test_result.status.value == DbtTestSuccessEnum.SUCCESS.value |
| assert kwargs["test_results"].timestamp.root == datetime_to_timestamp( | ||
| datetime(2026, 7, 24, 7, 0, 0), milliseconds=True | ||
| ) |
| assert test_case_result.timestamp.root == datetime_to_timestamp( | ||
| datetime(2026, 7, 24, 9, 0, 0), milliseconds=True | ||
| ) |
|
Verified — the finding was inaccurate on both points.
No code change needed here. |
Describe your changes:
Fixes #29824
I worked on the dbt test-result ingestion path because, after upgrading to 1.13.0, dbt test cases were still created from
manifest.jsonbut their results fromrun_results.jsonsilently never appeared.There are two independent defects behind the report, both 1.13.0-only regressions, and both are fixed here.
Defect 1 — the null-message guard was too broad. Commit
964ac7578271(#26812) added a guard inadd_dbt_test_result()that skips any result with a falsymessage, to filter out phantomdbt runentries. But dbt only populatesmessageon failure/warn, so a genuine passing data test emitsstatus="pass", message=nulland was dropped along with the stubs.DbtTestSuccessEnumdefines bothSUCCESS = "success"(compile-only) andPASS = "pass"(really executed); the guard checked neither.Defect 2 — compile-only stubs could shadow real results. Reported by
@ziggekattenin the issue thread and not covered by #29828._get_latest_result()picked among duplicateunique_identries purely byexecute.completed_at. Adbt docs generateartifact written after adbt testone wins on timestamp with itsstatus="success"stub, so the real result was discarded beforeadd_dbt_test_result()ever ran. This only affects blob-storage config sources —DbtLocalConfigandDbtHttpConfigstructurally cannot produce two run-results files.Both call sites now share one predicate,
is_compiled_only_result()(status == "success" AND not message), so the selector can never hand the ingester a result it will then drop — that divergence is the bug class.Relationship to #29828: this supersedes it. #29828 fixes Defect 1 only. Reverting just this PR's Defect 2 change reproduces #29828's exact shape, and the end-to-end test still fails
assert 0 == 1— so #29828 alone leaves S3/GCS/Azure deployments broken. Its RCA for Defect 1 is correct and was reused, with credit in the Defect 1 commit.Why
statusand notfailures:failuresis not inREQUIRED_RESULTS_KEYS, soremove_run_result_non_required_keys()strips it before parsing.statusis the only discriminator that survives. It is safe to dereference: all 9 run-results models incollate_dbt_artifacts_parserdeclarestatusasrequired=True, non-nullable, enum-typed.Type of change:
High-level design:
N/A — small change (3 files, +292/−5).
Tests:
Use cases covered
status="pass"andmessage=nullis ingested as a Success result (previously dropped).status="fail"/status="warn"with a null message.run_results.jsonfiles are read from one blob prefix, an executed result (pass/fail/warn) is preferred over a later compile-onlystatus="success"stub for the sameunique_id.executetimestamp.Unit tests
ingestion/tests/unit/test_dbt.py(+268, 12 new tests acrossTestAddDbtTestResultNullMessageandTestGetLatestResultPrefersExecutedResults)python -m pytest tests/unit/test_dbt.py→ 183 passedTestCaseResulthanded to the OpenMetadata client — status,result,testResultValue, timestamp and a realfqn-builttest_case_fqn— after running a realrun_results.jsonthrough the production key-stripper and the realcollate_dbt_artifacts_parser. Thefqnmodule is deliberately not patched, so the FQN assertion is real rather than tautological.Backend integration tests
Ingestion integration tests
Playwright (UI) tests
Manual testing performed
No live stack was used; these steps are for a reviewer who wants one. The two defects need different config sources — Defect 2 requires more than one
run_results.jsonin a singleDbtFiles, which only the blob-storage sources can produce. Using Local for Defect 2 will silently not reproduce it.Defect 1 (single artifact — Local is fine)
dbt teston a project (e.g.jaffle_shop). Confirmtarget/run_results.jsonhas a test node with"status": "pass","message": null,"failures": 0.dbtConfigSourcetype Local, settingdbtManifestFilePathanddbtRunResultsFilePath.metadata ingest -c <workflow>.yaml.Skipping compiled-only test result for '<node>' (message is null).Defect 2 (multiple artifacts — S3/GCS/Azure, or MinIO locally)
run_results— that substring is what makes the reader pick up both.s3://<bucket>/dbt/.dbtConfigSourcetype S3 (or GCS/Azure) at that prefix. Confirm from the debug log that both run-results files are read.metadata ingest -c <workflow>.yaml.dbt testrun, not the laterdbt docs generaterun.Negative check (both)
7. Run a job that only does
dbt runand confirm no bogus result rows appear — compiled-only stubs must still be skipped.UI screen recording / screenshots:
Not applicable.
Checklist:
Fixes <issue-number>: <short explanation>Fixes #<issue-number>above.Known limitations
success(skipped,partial success, and dbt Cloud'sno-op) with a null message are now ingested asAborted, where 1.13.0 dropped them. This is deliberate: it restores pre-Fixes #26785 - skip compiled-only dbt test results with null message #26812 (1.12.x) behaviour. Narrowing to an allow-list of executed statuses would re-introduce exactly the over-broad filtering that caused this issue.is_compiled_only_resultcarries onereportMissingParameterTypebasedpyright warning, matching 11 neighbouring helpers indbt_utils.py. There is no honest shared type — the parser exposes 9 unrelated result classes with no common base, and it is imported lazily insideget_dbt_objects().basedpyrightreports zero new errors versus the unmodified base.TestGetLatestResultclass buildsMagicMocks whose.messageis auto-truthy, so the new filter is a no-op there — those tests stay green for reasons unrelated to this change and are not selector coverage. The newTestGetLatestResultPrefersExecutedResultsclass uses really-parsed artifacts.🤖 Generated with Claude Code