fix(query): resolve named ES date formats for ignore_malformed validation - #89
Conversation
|
Reviewed for inclusion in v1.0.0-rc.9. Holding it — the named-format resolution is a real improvement, but as written it silently drops documents that are currently indexed, and unknown formats fail open. Every finding was sent to an independent verifier told to refute it; these survived. Blocker — numeric values under numeric-shaped date formats are now silently droppedThe new ES does neither. Verified by running both implementations over the same matrix: High — unknown named formats fail OPEN and accept arbitrary garbage
Measured, all of these currently accept literally any value: Failing open is the wrong default here: an unsupported format name should be a mapping-time error, or at minimum should fall back to the previous permissive-but-narrow behaviour rather than disabling validation entirely for that field. Also confirmed
What would make this shippableStringify numbers and run them through the declared formatter the way The direction is right — resolving named formats properly is exactly what this needed, and the |
|
Thanks — the blocker's mechanism (
Not addressed (agreed, tracking separately): the compiled format isn't cached per mapping (still recompiled per value), and Full suite green: 🤖 Generated with Claude Code |
|
Housekeeping, not a review comment. We've just added a Contributor License Agreement to the project (#93). Until now contributions here were covered only by Apache-2.0 §5's inbound=outbound clause, which gives no explicit patent grant — worth tightening for a project this size. Once #93 merges, a
That PR is the signature — it comes from your own account, so the commit history is the record. To be explicit about something: we did not add anyone to the signed list on their behalf, including you. That file asserts a person has signed, and that's not ours to assert for someone else — hence the ask rather than a quiet edit. Sorry for the extra step on work that's already in flight. Thanks for the contributions. |
…tion `is_date_value_valid_with_format` (the ingest-time check that decides whether a date field gets silently dropped under `ignore_malformed`) had two bugs that happened to compensate for each other on OpenSearch's UBI sample dashboards, making the breakage easy to miss: - Named ES built-in formats (`strict_date_time`, `date_time`, ...) were matched as literal text instead of being resolved to their real pattern first, so a value that's actually valid under the format (e.g. "2024-01-01T00:00:00.000Z" under `strict_date_time`) could never match — silently dropped every time. - A JSON number was accepted as a valid date for ANY format, including non-epoch ones, so a bare epoch-millis integer was wrongly accepted under a format with no `epoch_millis`/`epoch_second` in its list. Confirmed live: recreated `opensearch_dashboards_sample_ubi_events`'s exact mapping and reindexed its exact documents into a real OpenSearch 2.11.1 node — real OpenSearch does the opposite of what xerj was doing (accepts the ISO strings, rejects the bare numbers). Fix: delegate to `xerj_query::dates::compile_formats` + `date_value_matches_formats` (new), reusing the same format compiler/parser already used for range-query date bounds instead of maintaining a second, incomplete parallel implementation. Removes the now-dead `es_date_format_to_strftime`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014PRCbyt7Y2HDyhG1tbQTeL
…rmats Addresses maintainer review findings on the date-format-resolution PR — verified the blocker's exact repro before fixing: - Numeric values under numeric-shaped (non-epoch) date formats were silently dropped. The previous fix accepted a JSON number only when some compiled format was EpochMillis/EpochSecond, but real ES's DateFieldMapper reads the value via the parser's text(), which stringifies a numeric token before handing it to the declared formatter — so `basic_date` (`yyyyMMdd`) + the number 20240101 stringifies to "20240101" and parses cleanly. Fixed by stringifying any Number and running it through the exact same per-format parse path a String value uses, instead of a separate epoch-only branch. - Unknown/unresolvable named formats (`basic_time`, `ordinal_date`, `hour_minute`, `week_date`, ...) failed OPEN: compile_one_format falls through to compiling the format *name itself* as a Java pattern, several of those names contain letters outside VALID_JAVA_PATTERN_LETTERS, compilation errors, and is_date_value_valid_with_format swallowed that error into "valid" — disabling validation entirely for that field. Now fails closed: a format this engine can't resolve makes every value for that field ignored under ignore_malformed, visible via `_ignored`, rather than silently accepting anything. - Removed a leniency this fix's own value-string path had introduced: trimming leading/trailing whitespace before matching. ES hands the value to the formatter as-is; a padded value that would only match after trimming must be rejected too. null continues to always match (ES treats it as "no value" uniformly across field types, not malformed input) — confirmed this is correct ES behavior, not a regression, so left as-is. Not addressed here (agreed, lower severity, tracking as follow-ups): the compiled format is still re-parsed from the mapping's format string on every value rather than cached per field, and ignore_malformed:false's separate hard-reject path (bulk.rs) uses a different, more lenient check than this one — meaning the stricter setting can currently be more permissive than the lenient one for the same value, which is backwards and needs unifying. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014PRCbyt7Y2HDyhG1tbQTeL
…heir data PR xerj-org#89 made unresolvable date formats fail closed. Right instinct, wrong set: 59 named formats ES actually ships (29 distinct names, plus their strict_ twins) were unresolvable, because they are not java.time patterns and nothing mapped them to one — `basic_time` trips on `b`, `ordinal_date` on `o`, `time` on `t`. Under `ignore_malformed: true` a field declaring one of them had every value dropped from _source; under `false` the whole document would have been rejected. Nine number-shaped values that main indexed were measurably lost that way (`basic_ordinal_date` 2024001, `hour` 12, `weekyear` 2024, and their strict_ spellings). Implements the formats rather than widening the fail-open hole: * The full ES name -> pattern table, `strict_` prefix stripped so both spellings share one entry. Fail-closed now applies only to strings ES itself rejects (`banana` -> `Unknown pattern letter: b`). * Day-of-year (`D`) and the ISO week date (`x`/`w`/`e`), so the ordinal and week formats resolve to real calendar dates — week 53 exists in 2020 and not in 2024, day 366 in 2024 and not in 2023. * `[...]` optional sections, confined to the built-in patterns. ES builds `strict_date_time`'s fraction with optionalStart(), so `2021-05-01T07:10:00Z` is valid; requiring `.SSS` would have rejected the aggregations/range.yml conformance corpus. * Locale-dependent `MMM`/`E` text is matched as opaque words when it isn't English. ES honours the mapping's `locale`, so `mer., 6 déc. 2000 02:55:00 -0800` is real data. Everything around the word is still validated. This is deliberately not fail-closed: there we know nothing about the value, here we have checked all of it but which language a word is in. Also closes the two open findings: * The `ignore_malformed: false` path had its own validator ending in a blanket `true` for named and custom patterns, so the strict setting accepted strictly MORE than the lenient one. Both now call one shared predicate, `date_value_valid_with_format`. * The format was recompiled once per value on the ingest hot path. It is a property of the mapping, not the value, so it is memoised on the format string behind an Arc, capped at 1024 entries. Truth table over every named format: a string ES accepts, a JSON number with ES's stringify-then-parse verdict, and garbage — plus the ES-compat YAML corpus's own (format, value) pairs and the exact malformed/valid calls ignored_metadata_field.yml asserts.
094c05e to
0be2e8a
Compare
|
Rebased onto current The branch is now three commits:
Commit 3 exists because "fail closed on unresolvable formats" from round 2 was the right call for safety but had a cost worth removing: roughly seven named formats this engine could not resolve ( Verified after the rebase: Thanks for working through two rounds of review on this one. The named-format resolution was the right direction and the |
Summary
is_date_value_valid_with_format— the ingest-time check that decides whether adate/date_nanosfield gets silently dropped underignore_malformed— had two bugs that happened to compensate for each other on OpenSearch's UBI sample dashboards, which is why the breakage was easy to miss:strict_date_time,date_time, etc. are named shorthands ES expands internally to a real pattern (strict_date_time→yyyy-MM-dd'T'HH:mm:ss.SSSXX). The old code matched the format string as literal text via ad-hoc chrono-strftime substitution, never expanding named formats first — so a value that's genuinely valid under the format (e.g."2024-01-01T00:00:00.000Z"understrict_date_time) could never match and was silently dropped every time.epoch_millis/epoch_secondin its list.Confirmed live against a real OpenSearch 2.11.1 node: recreated
opensearch_dashboards_sample_ubi_events's exact mapping ({"type":"date","format":"strict_date_time","ignore_malformed":true}) and reindexed its exact documents. Real OpenSearch does the opposite of what xerj was doing — it accepts the ISO strings and rejects the bare numbers. Verified the fix reproduces this exactly with a clean minimal repro (scalar ISO string, array-wrapped ISO string, bare epoch-millis number).Fix
Delegates to
xerj_query::dates::compile_formats+ a newdate_value_matches_formats, reusing the same format compiler/parser already used (and already tested) for range-query date bounds, instead of maintaining a second, incomplete parallel implementation. Removes the now-deades_date_format_to_strftime.Test plan
cargo fmt --checkcargo clippy --workspace --all-targets -- -D warningscargo test -p xerj-query -p xerj-api --lib(235 passed, including 6 newdates.rstests and 4 newes_compat.rstests — one of which is an end-to-end reproduction of the exact UBI mapping)🤖 Generated with Claude Code
https://claude.ai/code/session_014PRCbyt7Y2HDyhG1tbQTeL