fix(sql,core): resolve temporal literals against the mapped date format before rendering the query (#276) - #289
Merged
Merged
Conversation
…at before rendering the query
A WHERE comparison against a `date`-mapped column forwarded the string literal verbatim into
the range/term query, so the SQL-standard spelling BI tools render
('2026-06-04 00:00:00.000000') was rejected by Elasticsearch's default
`strict_date_optional_time||epoch_millis` format while only the T-separated ISO form matched:
Superset's relative-date filter could not run, on JDBC and Flight SQL alike.
The literals a WHERE clause compares against a date column (= <> != >= > <= <, BETWEEN, IN)
are now resolved ONCE per statement, on the AST, against that column's mapping format
(`query.TemporalLiterals`): the space form is validated with java.time and rewritten with a T
(fraction digits and zone preserved); a literal a custom `format` already parses is left alone
so a `yyyy-MM-dd HH:mm:ss` column keeps working; epoch numbers and date math are untouched; a
literal that cannot be a date under a fully-understood format is rejected with a 400 naming the
literal and the field instead of a raw search_phase_execution_exception. keyword/text columns,
LIKE/RLIKE patterns, function-wrapped columns, TIME columns, date_nanos and HAVING are never
touched, and a keyword column compared to a date-shaped string emits byte-identical JSON.
No executing SELECT path attached a schema before (only CTAS's Table.mergeWithSearch did), so
the pass is invoked from core at every SingleSearch -> ElasticQuery seam - SearchApi.search /
searchAsync (single and each UNION ALL request), the inner-hits search, ScrollApi.scroll - through
the IndicesApi schema cache, with a per-client negative cache for sources whose schema cannot be
loaded. GatewayApi.run (REPL, JDBC, Flight SQL sidecar and its JOIN legs) is covered
transitively. Schema-absent paths (no candidate literal, several sources, wildcard, JOIN-alias
identifiers, unloadable schema) forward the literal verbatim as before.
Tests: TemporalLiteralsSpec (sql, 25), TemporalLiteralQuerySpec (bridge template + es6 copy, 6),
TemporalLiteralSearchSpec (core, 10, Docker-free), TemporalLiteralSpec testkit template + five
client subclasses green on ES 6.8 rest/jest, 7.17, 8.18, 9.0; falsified against the baseline core
seams (7/10 core, 4/7 on real ES 8.18 red with the original symptom).
Story BIDC-4
Closes #276
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…unded negative cache, DML WHERE Independent review of b15ce1a returned approve-with-fixes; every finding is addressed here. - R4-1 (HIGH): the non-strict `date_optional_time` is no longer rejection-eligible. Its lenient grammar (`2026-6-4`, `2026-06-04T1:02:03`, 5-digit years) is accepted by every Elasticsearch major and was being turned into a 400. The space-form rewrite still applies under it. - R4-2 (MEDIUM): rejection is conservative. A literal is rejected only when it cannot be an ISO date at all or carries an invalid recognised calendar/time component (`2026-02-30`, `T24:00:00`); any shape the recogniser does not model (zone ids, offsets with seconds, a bare `T`, signed years, ES 6.8's Joda leniencies) is forwarded verbatim and Elasticsearch decides. The accept set is pinned as a table in TemporalLiteralsSpec against the real DateFormatter parsers of ES 6.8.23 / 7.17.29 / 8.18.3 / 9.0.3 (56 literals; every REJECT fails on all four, every REWRITE is accepted on all four). - R4-3 (MEDIUM): the negative schema-miss cache remembers 404 misses only (a transient failure is retried), purges expired entries above 256 and clears above 1024 - the #238 shardCountCache discipline; unit-tested. - R4-4 (MEDIUM): DELETE / UPDATE ... WHERE get the same resolution as SELECT at the five by-query render sites of IndicesApi (SearchApi.resolveTemporalLiterals is private[client]); two integration cases with exact row oracles on a dedicated index. - R4-6 stale "date followed by a zone" claim dropped; R4-7 the head of a `||` date-math literal is normalised (`2026-06-04 10:30:15||+1d` -> `2026-06-04T10:30:15||+1d`); R4-9 docs: only JOIN-alias-qualified columns keep their literals; R4-12 licence headers on the sql/bridge test files. R4-5 (index aliases) stays a documented and release-noted exclusion pending the lead's ruling on getIndex semantics. Story BIDC-4 Closes #276 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rite, test ADT nested Second look on 6eeffeb approved; three LOW nits landed here. - R4-13: the rewrite's zone grammar is exactly the four offset shapes every Elasticsearch major accepts after a time (`+01`, `+0100`, `+01:00`, `+01:00:00`) plus `Z`; zone ids (`UTC`, `GMT+1`) and other offset shapes (`+010000`, `+0100:00`, `+01:0000`), which some or all majors reject, are never produced - their inputs stay verbatim. Five oracle rows added; the "every rewrite is accepted on all four" claim now states exactly what the rewrite can produce. - R4-16: the test-scope Expected / Verbatim / Reject / Rewrite types are nested in the spec's companion object instead of being public types of package sql.query. - R4-15: local deferred-work record updated (DML item fixed in 6eeffeb) - not a tracked file. Story BIDC-4 Closes #276 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…276, lead ruling on R4-5) A query through an index ALIAS got no temporal-literal normalisation: es8/es9 looked the alias up as a key in the GET-index response (which Elasticsearch keys by the CONCRETE index names) and es6/es7 built an empty schema from the same body, so the mapping never resolved and the old raw `search_phase_execution_exception` surfaced on the venue the issue reports. The rule lives in ONE place, `Index.apply(name, root)`: when the response carries no entry named `name`, an alias over exactly ONE index resolves to that index's document (the schema keeps the alias as its name); `Index.indexDocuments` exposes the members. `IndicesApi.loadIndexAsSchema` reports an alias over SEVERAL indices as not found rather than as an empty schema - uniform on every client - and es8/es9 `executeGetIndex` now hand the whole concrete-keyed map through instead of looking the requested name up as a key. es6/es7 rest and es6 jest return that body already and needed no client change. Audited consequences of the `getIndex` change, all deliberate: - the temporal-literal resolution now applies through a single-index alias (SELECT and DML alike); - `SHOW TABLE` / `DESCRIBE` through such an alias resolve instead of failing; - `INSERT INTO ... SELECT` and `COPY INTO` targeting a single-index alias resolve their metadata; targeting a MULTI-index alias they now fail with a named "not found" instead of silently using one member's mapping (es6/es7 previously proceeded on an empty schema); - a resolved alias is a schema-cache success, so the negative miss cache never remembers it. An alias over several indices stays a schema-absent boundary: the literal is forwarded verbatim, never rewritten and never rejected by us. The optional "merge when every member agrees" variant is deliberately out of scope. Tests: the resolution rule over a GET-index document with one / two members (sql, Docker-free); the core seam through an overridden `executeGetIndex` (single alias rewritten with zero cached misses, multi alias reported not found and verbatim); testkit `TemporalLiteralSpec` gains a single-alias case (three spellings via `search` and `run`) and a multi-alias case, green on ES 6.8 rest, 6.8 jest, 7.17, 8.18 and 9.0 (11/11 each). Story BIDC-4 Closes #276 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delta review of ab251b5 approved the design; two of its LOW findings are silent-failure shaped, so they are fixed here with the rest. - R4-18: the ambiguity test counted the documents that CARRY a `mappings` key, so a two-index alias whose other member has no mapping resolved silently to the first - the very silent-wrong-answer mode this story removes. `Index.indexDocuments` now returns every top-level index ENTRY and keeps the mappings/settings/aliases check only as the shape test that tells one index document from a map of them. Pinned: a two-member alias with a mapping-less member is reported NOT FOUND, never resolved. - R4-21: `invalidateSchema(<concrete>)` left a cached ALIAS schema in place, so after an ALTER TABLE a query through the alias kept a stale mapping - and a stale date `format` - for up to the 5-minute TTL. `Index` now carries the concrete name it resolved from, `IndicesApi` keeps an alias -> target map beside the schema cache, and invalidating (or updating) an index drops every alias entry pointing at it. Pinned: cache the alias, invalidate the concrete index, the next alias query re-fetches. - R4-19: the core seam spec now asserts that an ambiguous alias is BOUNDED - its 404 is remembered and a second statement performs no further lookup. - R4-20: the testkit multi-alias case asserts the literal is VERBATIM in the emitted query (unrewritten and unrejected) instead of tolerating any success. The alias now spans two custom-format indices so the query reaches Elasticsearch and its body can be asserted. - R4-17: a resolved alias is no longer listed among its own ALIASES, so `SHOW CREATE TABLE <alias>` does not render a self-referential entry. Story BIDC-4 Closes #276 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 #276
Story BIDC-4 (BI defect-closure epic). Branch
feature/BIDC-4offorigin/maine55bd937. Version line0.23.0-SNAPSHOTverified, untouched.What
WHERE event_ts >= '2026-06-04 00:00:00.000000'— the SQL-standard spelling Superset / SQLAlchemy render — was forwarded verbatim into therange/termquery and rejected by Elasticsearch's defaultdateformat (strict_date_optional_time||epoch_millis); only theT-separated form matched. Superset's relative-date filter could not run, on JDBC and Flight SQL alike.The string literals a
WHEREclause compares against adate-mapped column are now resolved against that column's mapping format ONCE per statement, on the AST, before the Elasticsearch query is rendered:sqlobjectquery.TemporalLiterals— walkswhere.criteria(Predicate,NESTED/CHILD/PARENTwrappers,GenericExpressionwith= <> != >= > <= <, literalBETWEEN, stringIN), resolves a function-free identifier throughschema.find, and rewrites theStringValuein place so the bridge receives the normalised value with zero bridge change and no second parse.LIKE/RLIKE, function-wrapped columns,TIMEcolumns,keyword/textcolumns,HAVINGandCASEare never touched.SearchApi.resolveTemporalLiteralsinvoked from everySingleSearch -> ElasticQueryconversion:search/searchAsync(single statement and eachUNION ALLrequest), the inner-hits search,ScrollApi.scroll— soGatewayApi.run(REPL, JDBC driver, Flight SQL sidecar and its cross-index JOIN legs, which re-entergateway.run) is covered transitively. Schema access = the existing 5-minuteIndicesApi.loadSchemacache via thethis match { case indices: IndicesApi => … }runtime pattern (SELECT without LIMIT returns only 10 rows on the non-scroll search path #209 precedent) — no signature change; the newSearchApitrait field (the negative cache) means the usual downstream rebuild on 0.23.0.WHEREofDELETE/UPDATE … WHEREat the five by-query render sites inIndicesApi(review R4-4), so a literal the SELECT accepts is accepted by the DML too.Format rule (AD-S4-1.1) — a strict-grammar APPROXIMATION, measured against all four ES parsers
format= the column's mappingformatelse the default; split on||. Per literal, in order: a number (leniently,+1/1e3included) ornow-anchored date math is verbatim; a||date-math literal has its HEAD normalised by the same rule and its math kept (2026-06-04 10:30:15||+1d→2026-06-04T10:30:15||+1d), never rejected; a literal a CUSTOM alternative already parses is verbatim (aformat: "yyyy-MM-dd HH:mm:ss"column keeps working — the fix does not invert the defect); if an ISO optional-time built-in is present, the RECOGNISED calendar shape (yyyy-MM-dd, optionallyTor ONE space,HH[:mm[:ss[.f]]], a zone) is validated withjava.time— the space form is rewritten toT(fraction digits and zone preserved, a lower-casezupper-cased), theTform is verbatim; a literal that merely STARTS like an ISO date (a signed or 5-digit year,2026-6-4,T1:02:03, an ordinal/week date, a zone id, an offset with seconds, a bareT, any unmodelled tail) is verbatim — Elasticsearch decides. A literal is rejected (400 naming literal, field and format) only when it cannot be an ISO date at all or carries an invalid recognised calendar/time component, and only under formats made solely of the STRICT ISO names and the epoch names; under the non-strictdate_optional_time, a custom pattern or an unrecognised built-in nothing is rejected. The accept set is pinned as a table inTemporalLiteralsSpecagainst the realDateFormatterparsers of ES 6.8.23, 7.17.29, 8.18.3 and 9.0.3 (56 literals + the four zone shapes below; every REJECT row fails on all four, every VERBATIM is a no-op, and every value the rewrite can PRODUCE — aTform with an optional fraction and an optionalZ/+01/+0100/+01:00/+01:00:00— is accepted on all four: zone ids (UTC,GMT+1) and other offset shapes (+010000,+0100:00,+01:0000), which some or all majors reject, are never produced and stay verbatim — second-look R4-13).Schema-absent path = verbatim (AD-S4-1.2), stated
The lookup is skipped when the
WHEREcarries no candidate literal (zero cost for the common statement), when theFROMdoes not name exactly one concrete index (several sources,*wildcard,,list), when the client is not anIndicesApi, and when the schema cannot be loaded (an index alias, an unknown index without a template, a cluster error, a thrown lookup). An identifier qualified with a cross-indexJOINalias is never resolved against the FROM table's schema. A cache miss costs oneGET <index>per index per 5 minutes, never per row — the lookup is synchronous on the caller thread (also onsearchAsync/scroll, like the DDL executors' ownloadSchemacalls) and not coalesced across concurrent first statements on a cold index (accepted trade-off, R4-10). A 404 miss is remembered for the same TTL in a per-client negative cache (bounded like #238'sshardCountCache: expired entries purged above 256, cleared above 1024), becauseloadSchemacaches successes only and an alias source on the es8/es9 clients would otherwise pay two round trips + two WARN lines per statement; a transient non-404 failure is never remembered. An un-LIMITedsearchAsyncrow query runs the pass twice (once insearchAsync, once in thescrollit routes to) — idempotent, one cache hit + one AST walk (R4-11).Spike results (T2 — measured, not argued)
SingleSearch.update(Some(schema))has exactly one production caller,Table.mergeWithSearch(CTAS/INSERT…SELECT target derivation).SearchApi.search/searchAsync/ScrollApi.scrollandGatewayApi's DQL executor never attach a schema (ecosystem-wide grep: sql/core/bridge/es*/macros/extensions/jdbc/arrow). AD-S4-1 as specced would have shipped with zero covered entry points; this PR adds the hop. A throwaway bridge probe (17 shapes, real parser + real bridge) showed: an operand CAN see the resolved column afterupdate(Some(schema))(GenericIdentifier.col = Some(event_ts DATE);colis not exposed on the sealedIdentifiertrait), a mappingformatsurvives intoColumn.optionsas aStringValue, and attaching the schema globally is emission-neutral on 13/13 parsing shapes. AD-S4-2 (dev decision, for the lead to veto): the hop is a statement-levelEither-returning pass invoked from core rather than a globalupdate(Some(schema))insideGenericExpression.update— AC 6 needs a rejection seam andupdate()cannot return one without athrow(forbidden after 21.4), and the pass does not re-run the wholeupdate()nor flipcol/outfor every identifier.JoinPlanner.planparses only to plan (JoinPlanner.scala:109); each leg is re-issued as a SQL string —SubQueryExecutor.scala:307→LocalConnection(gateway: GatewayApi) →ElasticSqlExecutor.scala:112/127/144gateway.run(sql)→ coreGatewayApi.run→SearchExecutor→api.searchAsync(single)/api.scroll(single). Legs re-enter the SAME core seam as direct queries; whatever the SELECT path does covers both venues.date_nanosresolves toSQLTypes.Anyfrom a mapping (SQLTypes.applydefault arm) and is therefore excluded by construction — pinned by a unit test over a realdate_nanosmapping document (AC 4: excluded, not covered).main(DATE_TRUNC(ts, DAY) >= '…',CAST(ts AS DATE) = '…'are parse-time type-mismatch rejections;DATE(ts)/CAST('…' AS DATE)in WHERE areend of input expected). Out of scope (function-wrapped operands never reachrangeQuery), recorded for 21.3/21.5.Covered entry points (AC 5)
SearchApi.search(single,UNION ALL)TemporalLiteralSearchSpec(core, Docker-free) +TemporalLiteralSpecon real ESSearchApi.searchAsyncTemporalLiteralSearchSpecScrollApi.scrollTemporalLiteralSearchSpec(rejection route) + un-LIMITed row queries inTemporalLiteralSpec(routed through scroll by #209)GatewayApi.run(REPL / JDBC / sidecar / JOIN legs)TemporalLiteralSpecgatewayIdson real ESTemporalLiteralSearchSpec(verbatim + lookup count 0/1)Tests
sql:TemporalLiteralsSpec(25) — format classification, every literal rule, statement walk incl. alias, AND/OR, NOT/!=/NOT IN, UNNEST nested wrapper, keyword/TIME/LIKE/function/unknown/custom-format negative controls, rejection messages (not startWith "Internal parser error"), real-mapping resolution incl.date_nanosexclusion.bridgetemplate + hand-maintainedes6/bridge:TemporalLiteralQuerySpec(6) — the GENERATED JSON:range/term/termscarry theTform; the keyword negative control is byte-identical with and without the schema (AC 3); epoch millis and the custom-format column verbatim.core:TemporalLiteralSearchSpec(14) — the seam on a recordingNopeClientApi: rewrite onsearch/searchAsync/UNION ALL, verbatim when the schema is absent / several sources / wildcard, zero lookups when no candidate, 400 rejection naming literal + field onsearch,searchAsyncandscroll(stream fails with the same error), the client never reached.TemporalLiteralSpec+ five client subclasses (es6 rest, es6 jest, es7, es8, es9): 8-row default-formatdateindex — the three spellings select the SAME idse4..e8(AC 1), equality/BETWEEN/IN/<in the space form, epoch millis (AC 4), keyword negative control on a date-shaped string (AC 3), customformat: "yyyy-MM-dd HH:mm:ss"index parity (AC 2), gateway venue, AC 6 rejection.SearchApi.scala/ScrollApi.scalareverted toorigin/main, 7/10 core seam tests go red with the original symptom (space form forwarded verbatim, no 400, scroll stream does not fail) and the 3 negative controls stay green; on real ES 8.18 the same revert turns 4/7 integration tests red with the issue's exact error (search_phase_execution_exception … all shards failed) while the epoch / keyword / custom-format controls stay green.zzone in the rewrite, T-form/date-only literals with invalid calendar values now rejected under fully-understood formats (AC 6 consistency), rejection literal bounded (120 chars) + control characters collapsed (20.4 log/SQLExceptiondiscipline),nowdate-math grammar tightened (nowhereis not date math), cross-indexJOIN-alias identifiers never resolved against the FROM schema, the negative-miss cache above, es6 bridge import. 2 pre-existing items deferred (below).Evidence
6eeffebe, after the review follow-up)sbt "+ sql/test"(2.12.20 + 2.13.16)TemporalLiteralsSpec24 incl. the 56-literal oracle table)sbt "core/test"(2.13.16) +++ 2.12.20 core/Test/compileTemporalLiteralSearchSpec12) · 2.12 compilessoftclient4es-sql-bridge/test/es6bridge/test/softclient4es7-sql-bridge/test/softclient4es8-sql-bridge/testsbt17 "softclient4es9-sql-bridge/test"++ 2.12.20 sql/Test/compilesbt17 "es6rest/testOnly *TemporalLiteralSpec"sbt17 "es6jest/testOnly *TemporalLiteralSpec"sbt17 "es7rest/testOnly *TemporalLiteralSpec"sbt17 "es8java/testOnly *TemporalLiteralSpec"sbt17 "es9java/testOnly *TemporalLiteralSpec"sql/testOnly *TemporalLiteralsSpec25/25,core/testOnly *TemporalLiteralSearchSpec14/14,++ 2.12.20Test/compile (sql + core),scalafmtAllalone, five ES legs ofTemporalLiteralSpecsql/testOnly *TemporalLiteralsSpec,core/testOnly *TemporalLiteralSearchSpec, bridges,++ 2.12.20test compile,es8java/compile, five ES legs ofTemporalLiteralSpec(now 11 cases)sql/test680/680,core/test881/881,++ 2.12.20Test/compile (sql + core) green,TemporalLiteralSpec11/11 on ES 6.8 rest, 6.8 jest, 7.17, 8.18 and 9.0origin/main)search_phase_execution_exception … all shards failed; epoch / keyword / custom-format controls greenDateFormatter.forPattern(spec).toDateMathParser()), 56 literals × {default,date_optional_time, nanos}elasticsearch-6.8.23(JDK 11),7.17.29(JDK 11),8.18.3+lucene-core-9.12.1(JDK 21) by the reviewer and9.0.3+lucene-core-10.1.0(JDK 21) for this follow-up — 9.0.3 identical to 8.18.3Release note (0.23.0) — AC 7
datecolumn ('2026-06-04 00:00:00','2026-06-04 00:00:00.000000', with or without a zone offset) now matches the same rows as the ISOTform on every venue (client API, REPL, JDBC, Flight SQL, JOIN legs). Previously it failed withsearch_phase_execution_exception … failed to parse date field.'not-a-date','2026-13-45 99:99:99','') now fails BEFORE reaching Elasticsearch with HTTP 400Cannot parse '<literal>' as a date/time value for date field '<field>' (mapping format '…'). Anything matching the raw Elasticsearch shard error text will not see it any more.keyword/textcolumns compared to a date-shaped string are byte-identical to before — the rewrite keys on the MAPPED type, never on the literal's shape. Custom-formatdatecolumns whose format already parses the literal are untouched.date_nanoscolumns are not normalised (they resolve toANYon the AST) — the space form still fails there as before.DELETE/UPDATE … WHEREget the same resolution asSELECT(the space form now deletes/updates the rows the SELECT matches; a literal the SELECT rejects is rejected with the same 400,operation = deleteByQuery|updateByQuery).a8b8453e): the rewrite emits only values every shipped major accepts — aTform with an optional fraction and an optionalZ/+01/+0100/+01:00/+01:00:00.'2026-06-04 10:30:15UTC'and other unmodelled zone shapes are forwarded verbatim and judged by Elasticsearch (7+ accepts a zone id, 6.8 does not), exactly as when the user types theTform.SHOW TABLE/DESCRIBE/INSERT INTO … SELECT/COPY INTOthrough such an alias (they used to fail "not found" on es8/es9 and see an empty schema on es6/es7). An alias over SEVERAL indices is ambiguous and is now reported as not found on every client (es6/es7 used to answer an empty schema): the literal is forwarded verbatim there, as before.WHEREcompares a string literal to a column and theFROMnames one concrete index, the index mapping is read through the schema cache (oneGET <index>per index every 5 minutes). Statements without such a predicate, multi-index/wildcard reads and unloadable schemas skip it and behave exactly as before.Aliases — lead ruling on R4-5: extend this PR (commit 4)
GET /<alias>answers with the concrete index document(s) keyed by THEIR names. The rule lives in ONE place,Index.apply(name, root): whenroothas no entry namedname, an alias over exactly one index resolves to that index's document (the schema keeps the alias as its name); an alias over several is ambiguous andIndicesApi.loadIndexAsSchemareports it not found (uniform on every client — es6/es7 REST and jest already returned the concrete-keyed body; es8/es9executeGetIndexnow hand the whole concrete-keyed map through instead of looking the alias up as a key). AuditedgetIndexconsumers (IndicesApi.scala:1219insertByQuery,:1458copyInto): both already mapNoneto a named "not found", so a single-index alias target now resolves its metadata (primary key, mappings) instead of failing, and a MULTI-index alias target now fails with that honest "not found" instead of silently proceeding on one member's mapping (es6/es7) or a 404-after-template (es8/es9).SHOW TABLE/DESCRIBEthrough a single-index alias resolve for the same reason. A resolved alias is a schema-cache SUCCESS, so the negative cache never remembers it as a miss; the cache is keyed by the alias name (a remap is seen after the 5-minute TTL). No optional "all members agree" merge for multi-index aliases — kept out of scope for safety. Tests:Index.applyrule (sql, GET-index JSON with one / two documents), the core seam through an overriddenexecuteGetIndex(single → rewritten, multi →getIndex=Noneand verbatim), andTemporalLiteralSpecalias cases (single alias: three spellings viasearch+run; two-index alias: no named 400) on the five clients.Delta review of the alias work (commit 5)
APPROVE, no HIGH/MEDIUM. Two of its LOW findings were silent-failure shaped and were fixed rather than recorded: R4-18 — the ambiguity test counted mapping-BEARING documents, so a two-index alias whose other member has no mapping resolved silently to the first; it now counts top-level index entries, with the mappings/settings/aliases check kept only as the shape test (one index document vs a map of them), pinned by a unit case. R4-21 —
invalidateSchema(<concrete>)left a cached ALIAS schema behind, so after anALTER TABLEa query through the alias kept a stale mapping (hence a staleformat) for up to the 5-minute TTL;Indexnow carries the concrete name it resolved from,IndicesApikeeps an alias → target map beside the schema cache, and invalidating or updating an index drops every alias entry pointing at it. Also R4-19 (a bounded ambiguous alias is now asserted: second statement, zero further lookups), R4-20 (the testkit multi-alias case asserts the literal is verbatim IN THE EMITTED QUERY — the alias spans two custom-format indices so the query reaches ES and its body can be read), R4-17 (a resolved alias is no longer listed among its own ALIASES, soSHOW CREATE TABLE <alias>has no self-referential entry) and R4-22 (these test counts). R4-14 stays recorded, not fixed: es6/es7 flatten any ≥ 400 onGET <index>into a synthetic 404 that is then remembered as a miss — pre-existing #184 family.Independent review follow-up (commits 2 and 3 on this branch)
Second look on
6eeffebe: APPROVE. Commit 3 lands the three LOW nits — R4-13 zone grammar tightened to the four offset shapes +Z(oracle rows added), R4-15 local deferred-work record updated, R4-16 test ADT nested in the spec. R4-14 (es6/es7 REST flatten any >= 400 onGET <index>to a synthetic 404, so such a status is remembered as a miss for 5 min on those clients) is pre-existing #184 territory — recorded in the findings log, not changed here.R4-1 false 400 under
date_optional_time(non-strict name dropped from the rejection-eligible set) · R4-2 conservative rejection (only "cannot be an ISO date" or invalid recognised component; unmodelled tails verbatim; oracle table on 4 majors incl. ES 9.0.3 run for this follow-up) · R4-3 negative cache bounded (404-only, purge > 256, clear > 1024) · R4-4 DMLWHEREwired at the five by-query render sites + 2 integration cases · R4-6 stale "date + zone" claim removed · R4-7||head normalised · R4-9 docs JOIN sentence · R4-12 licence headers on the sql/bridge test files · PR-body wording (R4-8/R4-10/R4-11).Coordination notes (T6)
quotedflag never enters the name comparison —GenericIdentifier.updateresolvescolwith the unquotednameand this pass keys onidentifier.namethe same way; the rewrittenwhere.sqlre-renders quoted names throughIdentifier.sqlunchanged.StringValue.valueAFTER lexing;''doubling changes only the lexer. Either order works; no shared file.Docs
documentation/sql/dql_statements.md— new### Temporal literals against date columnsunder## WHERE. Web MDX twin (softclient4es-web/src/content/docs/sql/dql.mdx) to be synced by the orchestrator (feedback_dual_docs_sync).🤖 Generated with Claude Code