fix(dbt): handle nanosecond-precision timestamps from dbt-fusion in test results#30481
fix(dbt): handle nanosecond-precision timestamps from dbt-fusion in test results#30481zak-nuccio wants to merge 1 commit into
Conversation
…est results dbt-fusion outputs 9-digit nanosecond timestamps (e.g. 2026-07-22T09:27:12.979492347Z) which don't match Python's %f (6-digit microseconds). Two sites affected: - add_dbt_test_result(strptime line): uncaught ValueError crashed test case ingestion for any dbt-fusion run result. - _execute_completed_at: silently returned None instead of parsing the timestamp, causing _get_latest_result to fall back to the wrong result. Both now strip the extra nanosecond digits to 6-digit precision on ValueError, preserving the correct timestamp. Adds test cases for both code paths.
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
| try: | ||
| return datetime.strptime(completed, DBT_RUN_RESULT_DATE_FORMAT) | ||
| except ValueError: | ||
| return None | ||
| dot = completed.rfind(".") | ||
| if dot != -1: | ||
| completed = completed[: dot + 7] + "Z" | ||
| return datetime.strptime(completed, DBT_RUN_RESULT_DATE_FORMAT) |
There was a problem hiding this comment.
⚠️ Bug: Fallback reparse in _execute_completed_at can crash ingestion
Previously the except ValueError in _execute_completed_at returned None, so any unparseable completed_at degraded gracefully and _get_latest_result fell back to matches[0]. Now the fallback path calls datetime.strptime(...) again with no guard: for any timestamp that fails to parse for a reason other than nanosecond precision (e.g. no fractional part, a +00:00 offset instead of Z, or a space instead of T), or where the reparse still fails, this second call raises an uncaught ValueError that propagates and crashes _get_latest_result. Wrap the fallback strptime in try/except and return None on failure to preserve the prior graceful behavior.
Return None if the truncated timestamp still fails to parse.:
try:
return datetime.strptime(completed, DBT_RUN_RESULT_DATE_FORMAT)
except ValueError:
dot = completed.rfind(".")
if dot != -1:
completed = completed[: dot + 7] + "Z"
try:
return datetime.strptime(completed, DBT_RUN_RESULT_DATE_FORMAT)
except ValueError:
return None
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| dot = completed.rfind(".") | ||
| if dot != -1: | ||
| completed = completed[: dot + 7] + "Z" | ||
| return datetime.strptime(completed, DBT_RUN_RESULT_DATE_FORMAT) |
There was a problem hiding this comment.
💡 Edge Case: Nanosecond fix assumes trailing 'Z' and drops timezone offsets
Both fallbacks slice to 6 fractional digits and unconditionally append a literal "Z" (completed[: dot + 7] + "Z"). This is correct for dbt-fusion's ...347Z form, but a timestamp with an explicit offset such as 2026-07-22T09:27:12.979492347+00:00 would be rewritten to ...492Z, silently discarding the offset and misrepresenting the instant. It also relies on there being at least 6 digits after the dot; fewer-than-6-digit fractions never reach this branch since %f accepts 1-6 digits, so the slice assumption holds only for the >6-digit case. Consider matching only the fractional-digit run rather than blindly appending Z.
Was this helpful? React with 👍 / 👎
Code Review
|
| Compact |
|
Was this helpful? React with 👍 / 👎 | Gitar
There was a problem hiding this comment.
Pull request overview
This PR updates the dbt ingestion logic to correctly parse dbt-fusion completed_at timestamps that include nanosecond precision (9 fractional digits), avoiding crashes and incorrect “latest result” selection when multiple run_results files contain the same unique_id.
Changes:
- Add nanosecond→microsecond truncation fallback when parsing dbt run result timestamps.
- Fix
_get_latest_resultselection by ensuringexecute.completed_attimestamps can be parsed consistently. - Add unit tests covering nanosecond timestamp handling in both
_get_latest_resultandadd_dbt_test_result.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| ingestion/src/metadata/ingestion/source/database/dbt/metadata.py | Adds a fallback parse path to truncate nanosecond timestamps to microsecond precision in two timestamp parsing sites. |
| ingestion/tests/unit/test_dbt.py | Introduces unit tests to validate nanosecond timestamp parsing behavior for latest-result selection and test result ingestion. |
Comments suppressed due to low confidence (1)
ingestion/tests/unit/test_dbt.py:3585
test_nanosecond_timestamp_is_parsedonly asserts thatadd_test_case_resultswas called, but it doesn’t verify that the nanosecond timestamp was actually parsed/truncated and propagated into the emittedTestCaseResult.timestamp. Adding an assertion on the computed timestamp makes the test validate the intended behavior (and not just “no crash”).
source.metadata.add_test_case_results.assert_called_once()
| except ValueError: | ||
| return None | ||
| dot = completed.rfind(".") | ||
| if dot != -1: | ||
| completed = completed[: dot + 7] + "Z" | ||
| return datetime.strptime(completed, DBT_RUN_RESULT_DATE_FORMAT) |
| def test_nanosecond_timestamp_parsed(self): | ||
| from metadata.ingestion.source.database.dbt.metadata import DbtSource | ||
|
|
||
| nano_result = self._make_result("test.pkg.my_test", "2026-07-22T09:27:12.979492347Z", "pass") | ||
| dbt_objects = self._make_dbt_objects([[nano_result]]) | ||
|
|
||
| got = DbtSource._get_latest_result(dbt_objects, "test.pkg.my_test") | ||
| self.assertIs(got, nano_result) |
| dot = completed.rfind(".") | ||
| if dot != -1: | ||
| completed = completed[: dot + 7] + "Z" | ||
| return datetime.strptime(completed, DBT_RUN_RESULT_DATE_FORMAT) |
There was a problem hiding this comment.
Invalid timestamps now skip tests
When multiple matching results include an unsupported timestamp, the unconditional second strptime raises instead of returning None, causing the entire test node to be skipped even when another matching result has a valid timestamp.
Knowledge Base Used: Ingestion Framework
| nano_result = self._make_result("test.pkg.my_test", "2026-07-22T09:27:12.979492347Z", "pass") | ||
| dbt_objects = self._make_dbt_objects([[nano_result]]) |
There was a problem hiding this comment.
Nanosecond parser remains untested
This fixture contains only one matching result, so _get_latest_result returns it before parsing the timestamp. Add at least two matches with nanosecond timestamps so the new normalization and latest-result ordering are actually exercised.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Fixes #30480
dbt-fusion outputs 9-digit nanosecond timestamps (e.g.
2026-07-22T09:27:12.979492347Z) which do not match Python%f(6-digit microseconds). Two sites were affected:add_dbt_test_result— uncaughtValueErrorcrashed test case ingestion for any dbt-fusion run result._execute_completed_at— silently returnedNoneinstead of parsing the timestamp, causing_get_latest_resultto fall back to the wrong result.Both now strip the extra nanosecond digits to 6-digit precision on
ValueError, preserving the correct timestamp.Greptile Summary
This PR adds dbt-fusion nanosecond timestamp normalization.
Confidence Score: 4/5
The timestamp fallback should be fixed before merging because an unsupported timestamp can now discard an otherwise usable dbt test node.
The retry correctly handles dbt-fusion nanoseconds, but its unguarded second parse regresses the established invalid-timestamp fallback, while the new latest-result test bypasses the parsing branch.
Files Needing Attention: ingestion/src/metadata/ingestion/source/database/dbt/metadata.py, ingestion/tests/unit/test_dbt.py
Important Files Changed
Reviews (1): Last reviewed commit: "fix(dbt): handle nanosecond-precision ti..." | Re-trigger Greptile