fix: parse nested and list temporal values with the inferred candidates - #164
Merged
Merged
Conversation
parse_xml typed <meta><date>03/15/2024</date></meta> as STRUCT(date DATE)
under datetime_format := 'us', then extracted NULL from it -- or aborted the
scan with a Conversion Error without ignore_errors. Repeated elements
inferred as DATE[] came back [NULL, NULL] the same way.
Inference and extraction disagreed about what a temporal value looks like.
InferTypeFromSamples decides a field is DATE by eliminating candidates from
a format list, but only a top-level column's winning format was ever
recorded: InferColumnType computes a nested field's winning format and
discards it, and LIST elements never carried one. Extraction then reached
ConvertToValue with an empty format and parsed with default ISO rules, which
reject the very values inference had just accepted.
Two halves, both needed:
- Thread the column's winning format through ExtractValueFromNode,
ExtractStructFromNode and ExtractListFromNode, so a LIST column's
elements convert with the column's own format.
- Where no format was recorded at all -- nested fields, whose format
inference discards -- retry a failed default parse against the same
candidate list inference used. That list now lives at file scope in
EffectiveDatetimeCandidates() and both sides call it, so the two cannot
drift apart again.
The fallback is consulted only after default parsing has failed, so values
that already parsed keep their meaning. It is not limited to an explicit
datetime_format: in auto mode the built-in candidates include %m/%d/%Y,
which is why inference types such a nested field DATE in the first place --
so extraction has to agree.
Verified against the pinned duckdb submodule (d8cdaa33fd, v1.5.5): the new
test passes 20 assertions, and the full suite is 3997/3998 assertions with
the only failure being the not-yet-fixed test for #158.
Closes #159
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoHU19qnngy1CJnXyX1csz
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The change is localized, aligns inference and extraction behavior, and includes targeted regression tests covering the reported failure modes and key controls.
Pull request overview
This PR fixes inconsistent temporal parsing in the parse_xml extraction path by ensuring nested STRUCT fields and LIST elements are converted using the same effective datetime candidates that schema inference used, resolving issue #159.
Changes:
- Centralized the effective datetime candidate list (
EffectiveDatetimeCandidates) so inference and extraction consult the same formats. - Threaded the column’s winning datetime format through recursive extraction helpers so LIST/STRUCT descendants can convert with the correct format when available.
- Added a fallback in temporal conversion: when default parsing fails and no explicit per-field format is available, retry parsing against the effective candidate list; added regression tests covering nested + repeated temporal shapes.
File summaries
| File | Description |
|---|---|
| test/sql/github_issue_159_datetime_format_nested.test | Adds regression coverage for nested STRUCT and repeated/LIST temporal parsing under both explicit and auto datetime modes. |
| src/xml_schema_inference.cpp | Unifies datetime candidates across inference/extraction, threads winning format through recursive extraction, and adds candidate-based fallback parsing for temporal conversions. |
| src/include/xml_schema_inference.hpp | Updates extractor helper signatures/docs to accept and propagate the threaded datetime format. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review caught that the candidate fallback added on this branch took the first
candidate that parsed, and the failure reproduced exactly as predicted:
data: 25/12/2024 (unambiguously day-first) + 01/02/2024 (ambiguous), auto mode
top level (format threaded): 2024-12-25, 2024-02-01 <- 1 February
nested (per-value fallback): 2024-12-25, 2024-01-02 <- 2 January
Same extension, same data, contradictory answers. Inference eliminates
candidates using EVERY sample in a column, so one unambiguous 25/12/2024
settles that column on %d/%m/%Y for good. A fallback looking at a single value
cannot repeat that elimination, and the auto list holds both %m/%d/%Y and
%d/%m/%Y with month-first first. A silently transposed date is worse than the
NULL this branch set out to remove.
TryParseTemporalWithCandidates now collects every candidate that parses and
returns a value only when they agree; when two disagree it declines and lets
the caller's conversion error stand. Its doc comment claimed "the winner is the
same one inference would have picked", which was simply false -- corrected.
This costs nothing for a user who names a format: the presets resolve to a
single candidate ('us' is {%m/%d/%Y}, 'eu' is {%d/%m/%Y}), so the rule cannot
fire for them. Declining also stays narrow -- an unambiguous nested value is
still parsed, so this is not a retreat to NULL for nested dates.
Measured after the change: ambiguous nested raises Conversion Error (NULL under
ignore_errors), explicit 'eu' resolves it to 2024-02-01 in agreement with top
level, an unambiguous nested value still parses, and the reporter's cases are
untouched. The test now pins the top-level reference alongside the nested
result, so a future change that makes them disagree fails here.
Test now 28 assertions; full suite 4004 assertions in 103 test cases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoHU19qnngy1CJnXyX1csz
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #159.
The report
Under
datetime_format := 'us', a top-level<date>03/15/2024</date>parses correctly, but the same value nested one level deeper does not:Cause
Inference and extraction disagreed about what a temporal value looks like.
InferTypeFromSamplesdecides a field is DATE by eliminating candidate formats against the samples — that is whytypeofalready answeredSTRUCT(date DATE)andDATE[]. The winning format, however, was only ever recorded for a top-level column:InferColumnTypecomputes a nested field's winning format intonested_winning_fmtand then discards it (the long-standingTODO(#38)).Extraction then reached
ConvertToValuewith an emptydatetime_formatand fell back to default ISO parsing, which rejects03/15/2024— the very value inference had just accepted. The column's type and the column's parser came from two different decisions.Fix
Two halves, both required — neither alone fixes both reported shapes:
Thread the format.
ExtractValueFromNode,ExtractStructFromNodeandExtractListFromNodenow take the column's winning format and pass it down, so a LIST column's elements convert with the column's own format. This fixes theDATE[]case, where the format was known and simply dropped.Fall back to the candidates, but decline when they disagree. Where no format was ever recorded — nested fields — a failed default parse is retried against the same candidate list inference used. This fixes the nested STRUCT case, which threading alone cannot reach because there is nothing to thread.
The candidate list now lives once, at file scope, in
EffectiveDatetimeCandidates(). BothInferTypeFromSamplesand the newTryParseTemporalWithCandidatescall it, so the two sides cannot drift apart again.The fallback is consulted only after default parsing has failed, so every value that parses today keeps its current meaning.
The ambiguity rule, and why it is not "first match wins"
An earlier revision of this PR took the first candidate that parsed. Review caught that this is a silent-corruption bug, and it reproduced exactly as predicted:
Same extension, same data, contradictory answers. Inference eliminates candidates using every sample in a column, so one unambiguous
25/12/2024settles that column on%d/%m/%Y. A fallback looking at a single value cannot repeat that elimination, and the auto list holds both%m/%d/%Yand%d/%m/%Ywith month-first first.So the fallback now declines when two candidates disagree about what the value means, letting the original conversion error stand. A silently transposed date is worse than the NULL this fix set out to remove.
This costs nothing for a user who names a format: the presets resolve to a single candidate (
'us'is exactly{%m/%d/%Y},'eu'is{%d/%m/%Y}), so the rule cannot fire for them.ignore_errors'eu'Declining stays narrow: an unambiguous nested value is still parsed, so this is not a retreat to NULL.
Not limited to an explicit format
Measured — in auto mode, with no
datetime_formatat all:The built-in candidates include
%m/%d/%Y, which is why inference types the field DATE in the first place.Covered
{'date': NULL}/ Conversion Error{'date': 2024-03-15}DATE[][NULL, NULL][2024-03-15, 2024-12-25]2024-03-15[NULL, NULL][2024-03-15, 2024-12-25]2024-03-15 14:30:00%d-%m-%Y2024-03-152024-03-15Controls that must not move, and do not: ISO input under a custom format still parses (default parsing runs first); a non-temporal nested field stays
STRUCT("label" VARCHAR);all_varchar := truestill wins over temporal typing. All five temporal branches (DATE, TIMESTAMP, TIMESTAMP_TZ, TIME, TIME_TZ) consult the fallback.Verification
Built against the pinned duckdb submodule (
d8cdaa33fd, v1.5.5).test/sql/github_issue_159_datetime_format_nested.test: all passing, now including the ambiguity cases and the top-level reference they must not contradict. Before the fix it failed on the nested case while its top-level control passed, so it distinguishes the two paths rather than failing everywhere.Every expectation was measured against a built binary before being written — including the wrong
2024-01-02above, which is why the ambiguity rule exists.🤖 Generated with Claude Code
https://claude.ai/code/session_01XoHU19qnngy1CJnXyX1csz