Repository navigation
fix(sql): a typed query answers one class per column, the type it reports - #443
Merged
Merged
Conversation
- `LAST_DAY(<adjuster>)` returned its RECEIVER's class: over a timestamp
`with(TemporalAdjusters.lastDayOfMonth())` kept the time of day, a
`ZonedDateTime` in a column reported DATE (`LAST_DAY(ts)` answered
`2024-02-29T23:30Z` where DuckDB answers `2024-02-29`), and a CASE mixing
it with a DATE arm answered two classes.
- In a query the receiver is now converted to a DATE before the adjuster,
from what it renders, through the one arm conversion
(`SQLTypeUtils.convertArm`): a date doc value through the UTC
normalisation (`….toInstant().atZone(ZoneId.of('Z')).toLocalDate()`,
which Elasticsearch 6.8 needs), a receiver that already renders a
`LocalDate` as it is. A function-wrapped operand is converted by the call
already, as before.
- `LAST_DAY` produces its type (`producesOutputJavaType`) and reports a
DATE, as DuckDB types `last_day(TIMESTAMP)`.
- An ingest processor and a transform keep their bytes (the ALTER-churn
rule): a type is context-free and date arithmetic, `GREATEST` / `LEAST`
read it, so while such a script renders `LAST_DAY` answers the type it
rendered before (`SQLTypeUtils.renderingIn`, set at the render entry
points). Without it 188 processor renders of a 13,000-statement corpus
moved; with it none does.
- A `date_nanos` operand types `Any`: its receiver is left as it renders.
Moved pins (LAST_DAY is now a DATE): DateDocValueEmissionSpec (LAST_DAY
over `d` and `ts`, the receiver-method mechanism), ParameterIdentitySpec
(`LAST_DAY(d) = d`), MixedTemporalComparisonSpec (`LAST_DAY(d) = d` is
promoted, `LAST_DAY(d) > CAST(ts AS DATE)` is two dates),
PredicateTransformSurvivalSpec (`LAST_DAY(d) = ts`,
`LAST_DAY(d) = LAST_DAY(ts)`).
Tests: LAST_DAY rows in DateDocValueEmissionSpec; in the testkit's
DateFunctionExecutionSpec, block "GATE-024 typed-path values", LAST_DAY
over a TIMESTAMP and over an undeclared date column against DuckDB's
values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss, the type it reports - A conditional's value took the arms' common `out`, and each arm was converted to it from its `baseType`. A schema-resolved arm's `out` and `baseType` are its COLUMN's runtime type whatever function it wears, so a CASE of two DATE arms was a TIMESTAMP: its `CAST(d AS DATE)` arm stayed a `LocalDate`, its DATE literal was widened to a `ZonedDateTime` and a bare DATE column stayed its doc value -- two classes in a column reported DATE, and a consumer (`DATE_TRUNC(CASE … END, MONTH)`) called `truncatedTo` on the `LocalDate` rows. COALESCE converted no arm at all (`COALESCE(CAST(n AS DOUBLE), 2)` answered the integer 2), NULLIF answered its first argument's doc value, and GREATEST / LEAST over dates a TIMESTAMP where DuckDB answers a DATE. - ONE rule now (`SQLTypeUtils.convertArm`): the node's type is the type it REPORTS, DuckDB's common type of what its arms report (DATE + DATE = DATE, DATE + TIMESTAMP = TIMESTAMP, INT + DOUBLE = DOUBLE), and in a query each arm is converted to it once, FROM what it renders. A CASE converts its results and ELSE, COALESCE every arm, NULLIF its answer, GREATEST / LEAST over dates the instant that wins; over numbers GREATEST / LEAST already converted every row to their reported type (`typedResult`). A consumer reads the type the node answers. A simple CASE's operand and WHEN values keep their comparison, NULLIF its comparison arm. - A date doc value converts through the UTC normalisation, never a bare `.toLocalDate()`; an arm narrowed already is not converted again; a conversion of a compound rendering applies to all of it, not to the last branch of a conditional (LAST_DAY over a CASE too). - Arms with no common type keep their bytes: numbers beside dates, text beside numbers, a TIME beside a date (DuckDB refuses them), an aggregate arm, and an arm whose type is unknown (no schema, `date_nanos`, `ip`). - An ingest processor and a transform keep their bytes: while one renders, a conditional answers the type it answered before. Without it 500 processor renders of a 13,000-statement corpus moved, some to a wrong answer (`CAST(COALESCE(CAST(d AS DATE), …) AS BIGINT)` lost its epoch milliseconds); with it none does. Moved pins: NullIfOperandSpec (`NULLIF(CAST(n AS KEYWORD), 'x')` reports the KEYWORD it answers; its comparison is asserted on the rendering), PainlessTargetSpec (`COALESCE(d, d2) > CAST(… AS DATE)`: no 6.8 normalisation is added, the arms convert through the UTC one), PredicateTransformSurvivalSpec (`COALESCE(d)` answers its DATE). - LAST_DAY over a nameless operand that can be NULL (`DATE_TRUNC(CASE … ELSE NULL END, MONTH)`) binds it once and converts and adjusts it inside a guard on it: appended to the guarded expression, the conversion and `.with(…)` ran on its NULL and failed the statement. - The type an ingest processor writes for an expression is read in the scope that processor renders in (`SQLTypeUtils.processorType`), by `ScriptProcessor.fromScript` and `ALTER COLUMN … SET SCRIPT`: read after the render had closed, it was the query's type (DATE for a CASE of two DATE arms, DOUBLE for `COALESCE(CAST(n AS DOUBLE), 2)`) over a script writing the legacy class. An UPDATE's one-off pipeline describes its processor as before. Tests: ConditionalArmClassSpec (every arm pair, consumers, the pairs with no common type, an unknown arm, an ingest processor's bytes); testkit rows in "GATE-024 typed-path values" against DuckDB's values, and eleven statements RIGHT on main whose script changes, which stay RIGHT. ConditionalArmClassSpec also pins LAST_DAY's guard over a nameless operand and the processor's written type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…EST is folded, not rendered into Painless - A NULL literal argument was rendered into the script as a NULL constant: `NULLIF(d, NULL)` compared `null != null && param1.isEqual(null)` and `LEAST(NULL, dn)` called `Math.min(null, param1)`, which Painless refuses to compile, so the statement failed; `DATE_ADD(COALESCE(CAST(d AS DATE), NULL), …)` called `.plus` on `(param2 != null ? param2 : null)`, a NullPointerException on every document missing `d`. - In a query the literal is now folded when the script renders: `NULLIF(x, NULL)` is `x`, `NULLIF(NULL, x)` is NULL, COALESCE drops a NULL arm, GREATEST / LEAST skip a NULL argument (DuckDB and PostgreSQL do), and with no argument left each is NULL. The folded node keeps the other argument's type and its NULL guard; NULLIF answers NULL where its arguments are equal, so a consumer guards it. - A method folded onto a bound expression that can be NULL (the cast a folded COALESCE hands `DATE_ADD`) is guarded, as an expression is: it landed outside the guard inside the expression. A CAST over a nameless conditional guards it too. - IFNULL and NVL do not parse in core: COALESCE is their spelling. - The statement, its `.sql` and an ingest processor's bytes are unchanged. - A CAST over a CASE converts the CASE as ONE value, bound once and under one NULL guard: appended to `p ? a : b`, the conversion converted the ELSE branch only, and `String.valueOf` around it answered the text "null" for its NULL rows (`CAST(CASE … DATE_PARSE(kd, …) ELSE CAST(d AS DATE) END AS VARCHAR)`). Tests: NullLiteralFoldSpec (each fold, the consumers' NULL guard, no NULL constant over the family, the SQL round trip, an ingest processor's bytes); testkit rows in "GATE-024 typed-path values" for `NULLIF(d, NULL)` and `DATETIME_ADD(LEAST(NULL, dn), INTERVAL 1 HOUR)` against DuckDB's values. NullLiteralFoldSpec also pins a cast over a CASE (VARCHAR and TIMESTAMP); its ingest-processor row names the twin it pins, a script that cannot compile (recorded, unchanged by ruling 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idnight - `CAST(<keyword> AS TIMESTAMP)` / `AS DATETIME` parsed with `DateTimeFormatter.ISO_DATE_TIME`, which requires a time, so a keyword holding `2024-01-03` threw on every document and failed the statement (`ISNOTNULL(CAST(kd AS TIMESTAMP))`), where DuckDB and PostgreSQL read the midnight of that day. - In a query a date-only value is now completed with `T00:00:00` before the same parse: one length test on the operand, bound once, per document; no formatter is built per document. A string literal is completed when the script renders. A date-time value parses as before. - An ingest processor keeps its bytes, so a pipeline does not churn on ALTER: there a date-only string still fails the cast, as before (`processorTemporal` already reads a date-only value of a DATE column). Tests: DateOnlyTimestampCastSpec (keyword to TIMESTAMP and DATETIME, a date-only and a date-time literal, the completed text against the formatter, an ingest processor's bytes); a testkit row in "GATE-024 typed-path values" for `ISNOTNULL(CAST(kd AS TIMESTAMP))` against DuckDB's values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… TIME its midnight, for every DATE operand
- Once a CASE, COALESCE, NULLIF, GREATEST or LEAST answered a DATE, a
cast of it to BIGINT or TIME fell to the cast's identity fallback and
answered the `LocalDate` itself under the BIGINT / TIME label. Before,
the conditional was a TIMESTAMP and the cast called `toInstant()` on a
`LocalDate`, which failed the statement
(`CAST(COALESCE(CAST(d AS DATE), CAST('2024-01-01' AS DATE)) AS BIGINT)`),
or converted the instant GREATEST / LEAST chose.
- `CAST(CURRENT_DATE AS BIGINT)` and `CAST(CAST('2024-01-03' AS DATE) AS
BIGINT)` answered the date itself: the cast sets its operand's `out` to
the target, so the conversion read BIGINT -> BIGINT. `CAST(CAST(ts AS
DATE) AS BIGINT)` and `CAST(LAST_DAY(ts) AS BIGINT)` converted from the
column's TIMESTAMP and called `toInstant()` on a `LocalDate`.
- The cast now has one rule for an operand that renders a DATE: to BIGINT
it is the epoch milliseconds of its UTC midnight, to TIME that midnight
(`00:00:00`). What the operand renders is read from its own chain: a
conditional the type it answers once its arms are converted to it, a
function that only adjusts a date (DATE_ADD, DATE_TRUNC, an interval)
its receiver's class, any other function its own type. The operand is
converted as one value, bound once and guarded, since a DATE CASE
answers NULL without being nullable.
- A conditional whose arms are left as they render (no common type, or
an arm of a type still unknown: no schema, `date_nanos`) renders no
single class and keeps its conversion.
- A DATE column renders its doc value, an instant, and keeps the
conversions of that instant: `CAST(d AS BIGINT)` and `CAST(d AS TIME)`
render as before.
- Query context only: an ingest processor and a transform keep their
bytes. DATE -> INT / DOUBLE and TIMESTAMP -> BIGINT are unchanged.
- What the operand renders is ONE derivation (`SQLTypeUtils.chainRendered`)
that reads each conditional's own arm decision over its own arms
(`AnswersArms.answeredArms`, `SQLTypeUtils.armsAnswer`), not a
restatement of it.
Tests: DateCastMidnightSpec (a DATE column, a cast to DATE, CURRENT_DATE,
a DATE literal, CASE, COALESCE, NULLIF, GREATEST, LEAST and LAST_DAY,
each to BIGINT and TIME, with the type they report; date functions over a
DATE; an ingest processor's and a transform's bytes); a testkit row in
"GATE-024 typed-path values" asserting the UTC-midnight values over the
4-document table. The testkit block also gains the rows for LAST_DAY's
NULL guard and a cast over a CASE (fixed in the second and third
commits).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sub-day interval; whole days over a DATE report DATE - HOUR / MINUTE / SECOND / the sub-second parts / OFFSET_SECONDS and `EXTRACT(… FROM …)` of an operand that renders a DATE (a `LocalDate`: a cast to DATE, LAST_DAY, a conditional that answers a DATE) read it at its UTC midnight, as DuckDB reads `hour(DATE)` (0). On the `LocalDate` itself `get(ChronoField.HOUR_OF_DAY)` failed the statement: on `main` for `HOUR(CAST(d AS DATE))`, and for `OFFSET_SECONDS(LAST_DAY(ts))` once LAST_DAY answered its DATE. - A sub-day interval (`HOUR`, `MINUTE`, `SECOND`) over an operand that renders a DATE starts from its UTC midnight and is a TIMESTAMP, as DuckDB types `DATE + INTERVAL 1 HOUR`: `LocalDate.plus(1, HOURS)` failed the statement for a DATE GREATEST / COALESCE, while the same call over `CAST(ts AS DATE)` was promoted. - DATE_ADD / DATE_SUB / DATETIME_ADD / DATETIME_SUB over an operand that REPORTS a DATE report a DATE for a whole number of days (DAY, WEEK, MONTH, QUARTER, YEAR), the date-arithmetic ruling (a date plus whole days is a date; DuckDB 1.5.5 types it TIMESTAMP), and a TIMESTAMP for a sub-day interval. Over a bare DATE column `DATE_ADD(d, INTERVAL 1 DAY)` reported its runtime TIMESTAMP, so a COALESCE beside a cast to DATE converted that cast to a `ZonedDateTime`. - Every consumer reads what its operand renders through the one chain derivation (`SQLTypeUtils.operandRendersDate`), the one a cast to BIGINT / TIME reads. - In a query only: an ingest processor and a transform keep their bytes. Moved pins: - ParameterIdentitySpec: a CASE of `DATE_ADD(d, INTERVAL 1 DAY)` and `d` answers a DATE, each arm converted once; the interval still folds onto its own column's parameter. - SQLQuerySpec (both bridges): in `TRY_CAST(COALESCE(NULLIF(createdAt, …), CURRENT_DATE - INTERVAL 2 HOUR) AS BIGINT)` the DATE minus two hours starts from its UTC midnight, and the TIMESTAMP is converted to its epoch milliseconds. It was `LocalDate.minus(2, HOURS)`, which throws, and the TRY_CAST swallowed that into NULL. Tests: DateOperandSpec (each time-of-day part over each DATE producer, a date part and a TIMESTAMP left alone, the sub-day intervals, the reported types, the COALESCE beside `DATE_ADD`, an ingest processor's bytes); testkit rows in "GATE-024 typed-path values" against DuckDB's values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d, as DuckDB refuses it - Once the types are read, a CASE or a COALESCE whose arms mix a date (DATE, TIMESTAMP) or a TIME with a number, a date with a TIME, text with a number or a date, or a BOOLEAN with a date, is refused with DuckDB 1.5.5's reason: `Cannot mix values of type INT and DATE in CASE expression - an explicit cast is required` (`… in COALESCE operator …`). Rendered, such a node answered two classes in one column, or failed in Painless. - Read on what each arm reports. A BOOLEAN beside a number is accepted (DuckDB types it an INTEGER); a NULL, a string literal (read as the type beside it), an arm of a type still unknown and an aggregate arm are left out, as before. NULLIF keeps its comparison rule, which already refuses arguments with no common type. Tests: MixedArmsRefusalSpec (each refused pair with its reason, the accepted pairs); a testkit row in "GATE-024 typed-path values". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he statement, as DuckDB does - `Math` answered `NaN` or `-Infinity` for a value outside the function's domain, and the column read the text "NaN". DuckDB 1.5.5 fails the statement: `ASIN is undefined outside [-1,1]`, `cannot take logarithm of zero`, `cannot take logarithm of a negative number`, `cannot take square root of a negative number` (measured). - In a query the argument is bound once and tested; a value outside throws an illegal argument with DuckDB's reason (`Optional.empty().orElseThrow(() -> new IllegalArgumentException(<reason>))`: Painless has no `throw` expression), so Elasticsearch fails the search with that reason as the cause of each shard failure. The status is the server's: 400 on Elasticsearch 8.18.3, 500 on 6.8.23 (measured). - An ingest processor and a transform keep their bytes. Moved pins: SQLQuerySpec (both bridges): the SQRT, LOG, LOG10, ASIN and ACOS script fields carry the domain test. Tests: MathDomainSpec (each function's test and reason, the functions with no domain to check, an ingest processor's bytes); a testkit row in "GATE-024 typed-path values": through `GatewayApi.run`, each statement streamed (its rows read to the end) and with a LIMIT (a search), the failure, the server's status where the client carries it and the reason where the client carries it; the values inside the domain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the receiver's runtime class - A `date_nanos` column types `Any`, so the arm conversion left LAST_DAY's receiver as it renders, and the adjuster kept its class: `LAST_DAY(dn)` answered a `ZonedDateTime` with its time of day in a column reported DATE. - In a query, a receiver whose type is unknown is made a DATE from its runtime class, once per row: a `LocalDate` as it is, a `LocalDateTime` its date, a zoned value its UTC date (`LastDayOfMonth.dateOf`). A nameless receiver is bound once. - The test is the arm conversion's own: `SQLTypeUtils.armSourceType` is unknown, the case where `convertArm` leaves a receiver as it renders. A typed receiver is converted as before, byte for byte; an ingest processor and a transform keep their bytes. - Reach, over a 13,289-statement render corpus (25,121 renders per mode): 17 query renders move, every one a LAST_DAY over a `date_nanos` operand (`dn`, or a CASE, DATE_TRUNC or GREATEST over it); 0 on the Elasticsearch 6.8 target, where the field is a plain `date`; 0 processor and 0 transform renders. Moved pin: DateDocValueEmissionSpec (LAST_DAY over a date_nanos operand). Tests: DateDocValueEmissionSpec (the dispatch, a nameless operand bound once, no dispatch over a typed receiver); a testkit row in "GATE-024 typed-path values" (an index, a pattern, a GROUP BY key whose count is compared by value). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ison
A conditional's nameless identifier answers `nullable = false` whatever its
arguments hold, so a conversion of what a CASE, a COALESCE, a NULLIF, a
GREATEST or a LEAST renders was appended to the conditional's own NULL
guard instead of being placed inside one, and ran on its NULL.
`WHERE GREATEST(d, d2) > d` rendered
`(lv3 == null ? null : ….toLocalDate()).atStartOfDay(ZoneId.of('Z'))`, and
every document missing both dates failed the statement on Elasticsearch
8.18.3 and 9.0.3; a CASE (`p3 ? a : b`) had its last branch converted only.
- One answer, `SQLTypeUtils.mayRenderNull`: an operand's own nullability or,
in a query, its function's. Every consumer that converts such an operand
reads it, and `coerce` binds the operand once and converts it inside its
guard:
- a comparison's operand, converted as ONE value
(`GREATEST(d, d2) > d`, `COALESCE(d, d2) > ts`, `NULLIF(d, d2) > ts`,
`CASE … END > ts`); a text comparison stringifies it inside its guard
too, where `String.valueOf` of a NULL was the text "null"
(`COALESCE(d) = 'x'`);
- a CASE arm (`CASE WHEN n > 0 THEN GREATEST(d, d2) ELSE ts END`) and the
last COALESCE arm (`COALESCE(ts, GREATEST(d, d2))`);
- a GREATEST / LEAST argument (`GREATEST(COALESCE(d, d2), ts)` converted
the COALESCE to epoch milliseconds unguarded);
- a NULLIF answer converted beside a numeric comparison (`NULLIF(d, 2)`
rendered `p1 == 2 ? null : p1.toInstant()…`);
- LAST_DAY's receiver, which asked the same question inline.
- An ingest processor and a transform keep their bytes.
Tests: ConditionalArmClassSpec pins the null-safe Painless of each site
and PredicateTransformSurvivalSpec the guarded one-argument COALESCE;
DateArithmeticSpec runs the comparisons (COALESCE, NULLIF, CASE beside a
TIMESTAMP) and the projections (a CASE arm, the last COALESCE arm, a
GREATEST argument) against java.time on a fixture row with no date at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an instant, the DATE taken at its midnight A TIMESTAMP column compared with a DATE conditional was narrowed to its day. `WHERE ts > CASE WHEN n > 0 THEN d ELSE d2 END` rendered `doc['ts'].value.toLocalDate()`, so `2024-01-31T10:30Z` was not after `2024-01-31`, where DuckDB answers TRUE; COALESCE, NULLIF, GREATEST, LEAST and LAST_DAY over a conditional did the same. The narrowing exists for a DATE column beside a DATE literal. It fires whenever the right-hand side reads no document field, which a conditional's nameless identifier does not, so it fired as soon as a conditional reported DATE. A DATE literal and CURRENT_DATE inside a script were narrowed the same way. - `narrowsToValueType` no longer narrows, in a query, an operand that reports a TIMESTAMP or a DATETIME read from a column declared as one. The DATE side is promoted to its UTC midnight instead, inside its NULL guard, as it already was with the DATE on the left. The narrowing injection reads the same predicate. A DATE against a DATE, a TIME, a chain over a DATE column (`DATE_TRUNC(d, MONTH)`) and a column of unknown type keep their bytes. - A comparison renders in the scope of its context (`SQLTypeUtils.renderingIn`). In a transform, `COALESCE(d2, d) > ts` read the query's DATE for arms that render their doc values there and appended `.atStartOfDay(…)` to a `ZonedDateTime`. Transform and ingest processor renders are now those of the main branch again. Tests: ConditionalArmClassSpec pins the Painless of a TIMESTAMP beside a CASE, a COALESCE, a NULLIF, a GREATEST, a LEAST and a LAST_DAY, on both sides, for the six operators, plus a DATE literal and two transform comparisons. DateArithmeticSpec runs 600 comparisons of a TIMESTAMP (a column, a literal at midnight or at a time of day) with a DATE expression (a column, a CAST, a conditional, LAST_DAY, DATE_TRUNC), on both sides, for the six operators and over NULLs, against DuckDB 1.5.5's answers; 72 of them, a column against a column, run on 7 and up only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the other majors The ES 6 bridge only asked its LEFT operand whether a comparison needs a script. A column compared with another column, or with a function of one (`ts < d`, `d2 > ts`, `ts > CAST(ts AS DATE)`, `ts2 < DATE_TRUNC(d, MONTH)`), therefore ran as a `range` query bounded by the other column's NAME, and Elasticsearch 6.8 answered `failed to parse date field [d]`. The template bridge has asked both operands since #373; the hand-maintained ES 6 bridge never received that rule. - `requiresScript(identifier, maybeValue)` is ported unchanged to the ES 6 bridge and read by `expressionToQuery`, as in the template. A comparison with a constant keeps its query; `IN` and `BETWEEN` are untouched, as in the template. Tests: DateArithmeticSpec's 600 comparisons of a TIMESTAMP with a DATE expression now all run on every major; the 72 column-against-column ones ran on 7 and up only. On Elasticsearch 6.8.23 all 600 keep the documents DuckDB 1.5.5 keeps (0 wrong, 0 errors), and the whole suite passes there (30 tests). es6bridge unit tests: 326 passed. Refs #443 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fupelaqu
force-pushed
the
fix/typed-path-values
branch
from
October 22, 2026 07:40
6300ade to
a611ae0
Compare
… refuses with a 400 The test relied on a script error to get a 400. Since the date-only cast now compiles on Elasticsearch 6.8, that script fails at run time instead, and 6.8 answers a Painless runtime error with a 500. The test now compares an integer field with a value that is not a number: every shard rejects the range query while building it, before any script runs, with a 400 on every major. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…page says why its shards failed Elasticsearch gives why a shard failed (`ASIN is undefined outside [-1,1]` under a script's `runtime error`) only under `failed_shards[i].reason.caused_by`; the `root_cause` entries do not carry it. The ES 8 / ES 9 client appended it, the REST clients did not: on Elasticsearch 7, a streamed SELECT failing on a function outside its domain answered "all shards failed: runtime error" only. One function in core, `ShardFailures.shardReason`, now reads it for every client: the REST clients append it to a rejected page's message, and the Java clients use it in place of their own copy (same message as before). The test of a function outside its domain now asserts the reason on every major: every client carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fupelaqu
marked this pull request as ready for review
October 22, 2026 14:35
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.
fix(sql): a typed query answers one class per column, the type it reports
Summary
On the Painless path (script fields, sort scripts,
GROUP BYkey scripts, script predicates) a schema-typed query could answer a value whose class was not the type the column reported. It could also fail outright where DuckDB and PostgreSQL answer:CASE WHEN n > 0 THEN CAST(d AS DATE) ELSE CAST('2024-01-01' AS DATE) ENDreported DATE. The cast arm answered aLocalDate, the DATE literal was widened to aZonedDateTime, and a bare DATE column stayed its doc value.LocalDatemethod on the other rows, or the reverse, and failed. Example:DATE_TRUNC(CASE … END, MONTH)calledtruncatedToon aLocalDate.COALESCEconverted no arm:COALESCE(CAST(n AS DOUBLE), 2)answered the integer2.NULLIFanswered its first argument's doc value.GREATEST/LEASTover dates answered a TIMESTAMP where DuckDB answers a DATE.LAST_DAYkept its operand's class.LAST_DAY(ts)answered2024-02-29T23:30Zwhere DuckDB answers2024-02-29.date_nanoscolumn, whose type is unknown to the schema, it answered aZonedDateTimewith its time of day.NaNor-Infinity, which the column read as the text"NaN". DuckDB fails the statement.NULLIF(d, NULL)andLEAST(NULL, dn)producednull != null && …andMath.min(null, …), which Painless refuses to compile.DATE_ADD(COALESCE(CAST(d AS DATE), NULL), …)called.pluson a possibly-null value.CAST(kd AS TIMESTAMP)over a keyword holding2024-01-03threw on every document and failed the statement. DuckDB and PostgreSQL read the midnight of that day.CAST(CURRENT_DATE AS BIGINT)andCAST(CAST('2024-01-03' AS DATE) AS BIGINT)answered theLocalDateunder the BIGINT label.CAST(CAST(ts AS DATE) AS BIGINT),CAST(LAST_DAY(ts) AS BIGINT)and a DATE conditional under a cast to BIGINT or TIME (CAST(COALESCE(CAST(d AS DATE), CAST('2024-01-01' AS DATE)) AS BIGINT)) calledtoInstant()on aLocalDateand failed the statement.Root cause
A conditional took its arms' common
outand converted each arm from itsbaseType. A schema-resolved arm'soutandbaseTypeare its column's runtime type, whatever function the arm wears. So a CASE of two DATE arms over adatecolumn was a TIMESTAMP that converted nothing.LAST_DAYappliedwith(lastDayOfMonth())to whatever its receiver was. NULL literals went through the same rendering as any other argument.The fix
Nine commits:
LAST_DAYanswers a DATE whatever its operand.….toInstant().atZone(ZoneId.of('Z')).toLocalDate()), and a receiver that already renders aLocalDateis used as it is.LAST_DAYreports DATE, as DuckDB typeslast_day(TIMESTAMP).A CASE, COALESCE, NULLIF, GREATEST or LEAST answers one class: the type it reports.
SQLTypeUtils.convertArm. The node's type is DuckDB's common type of what its arms report: DATE + DATE = DATE, DATE + TIMESTAMP = TIMESTAMP, INT + DOUBLE = DOUBLE.typedResult, the parent fix). That is the same rule, so the two halves agree: one common reported type, rendered by a cast..toLocalDate(). An arm that is already narrowed is not converted again. A conversion of a compound rendering applies to all of it, not to the last branch of a conditional.date_nanos,ip) keep their bytes.DATE_TRUNC(CASE … ELSE NULL END, MONTH)) binds it once and converts and adjusts it inside a guard on it; appended to the guarded expression,.with(…)ran on its NULL.SQLTypeUtils.processorType, read byScriptProcessor.fromScript,ALTER COLUMN … SET SCRIPTand the extensions' scripted view fields). An UPDATE's one-off pipeline description is unchanged.A NULL literal argument is folded, not rendered (and a CAST over a CASE converts every branch).
NULLIF(x, NULL)isxandNULLIF(NULL, x)is NULL. COALESCE drops a NULL arm, and GREATEST / LEAST skip a NULL argument (DuckDB and PostgreSQL do the same). With no argument left, each is NULL.p ? a : b, the conversion converted the ELSE branch only, andString.valueOfaround it answered the text "null" for its NULL rows. This closes the recorded "a CAST over a CASE converts its last branch only": 24 statements on ES 8 and 28 on ES 6.8 converted the last branch only on the round-1 tip, 0 now.A date-only string casts to TIMESTAMP and DATETIME as its midnight.
T00:00:00when it is 10 characters long, before the same parse. This is one length test per document, with no formatter built per document.A DATE cast to BIGINT answers its epoch milliseconds, and to TIME its midnight, for every DATE operand (the lead's ruling).
CAST(<DATE> AS BIGINT)is the epoch milliseconds of the value's UTC midnight;CAST(<DATE> AS TIME)is00:00:00.Conversion.convertsDate, converted byConversion.dateAtMidnight), for every operand that renders a DATE: a cast to DATE,CURRENT_DATE, a DATE literal,LAST_DAY, and a CASE, COALESCE, NULLIF, GREATEST or LEAST that answers a DATE.Conversion.rendersDate): a conditional renders the type it answers, a function that only adjusts a date (DATE_ADD,DATE_TRUNC, an interval) renders its receiver's class, and any other function renders its own type. The operand'soutcannot answer, because the cast sets it to the target:CAST(CURRENT_DATE AS BIGINT)read BIGINT -> BIGINT and converted nothing.SQLTypeUtils.armsTypeover the arms it answers). A conditional whose arms are left as they render (no common type, or an arm of a type still unknown: no schema,date_nanos) renders no single class, and the cast keeps its conversion. Without this, the bridge pinTRY_CAST(COALESCE(NULLIF(createdAt, DATE_PARSE(…)), CURRENT_DATE - INTERVAL 2 HOUR) BIGINT)(no schema,SQLQuerySpec) moved. It is unchanged.nullable.main's answers:CAST(d AS BIGINT)is the stored instant's epoch milliseconds, andCAST(d AS TIME)that instant's UTC time of day.Unimplemented type for cast (DATE -> BIGINT),(DATE -> TIME)), so the values are the rule's.A DATE operand is its UTC midnight under a time of day or a sub-day interval; whole days over a DATE report DATE.
EXTRACT(… FROM …)of an operand that renders a DATE read it at its UTC midnight, as DuckDB readshour(DATE)(0). This also repairs the pre-existingHOUR(CAST(d AS DATE)), which failed onmain.DATE + INTERVAL 1 HOUR, as DuckDB types it).DATE_ADD/DATE_SUB/DATETIME_ADD/DATETIME_SUBover an operand that reports a DATE report a DATE for whole days (DAY, WEEK, MONTH, QUARTER, YEAR) and a TIMESTAMP sub-day. DuckDB 1.5.5 typesDATE + INTERVAL 1 DAYa TIMESTAMP; the lead's date-arithmetic ruling (a date plus whole days is a date) and the gate oracle'sCAST(d + INTERVAL 1 DAY AS DATE)win.SQLTypeUtils.chainRendered/operandRendersDate), the one the cast to BIGINT / TIME reads; that cast now reads each conditional's own arm decision (AnswersArms,SQLTypeUtils.armsAnswer, fifth commit) rather than restating it.A CASE or COALESCE whose arms have no common type is refused, after the types are read, with DuckDB 1.5.5's reason (
Cannot mix values of type INT and DATE in CASE expression - an explicit cast is required,… in COALESCE operator …). A BOOLEAN beside a number is accepted (DuckDB: INTEGER); NULL, string literals, unknown and aggregate arms are left out. NULLIF keeps its comparison rule, which already refuses such arguments. Over the 13,289-statement corpus it fires on 1,826 statements: 1,802 are ones DuckDB refuses (ORACLE-REFUSES-TYPE), 24 carry aniparm (no oracle); none is a statement DuckDB answers.ASIN, ACOS, LOG, LOG10 and SQRT outside their domain fail the statement, as DuckDB does.
IllegalArgumentExceptionwith DuckDB's reason:ASIN is undefined outside [-1,1],cannot take logarithm of zero/of a negative number,cannot take square root of a negative number.throwexpression, so the throw isOptional.empty().orElseThrow(() -> new IllegalArgumentException('<reason>')).caused_byof each shard failure. The status is the server's, not the throw's: ES 8.18.3 answers 400, ES 6.8.23 answers 500 (measured in the testkit row below). The earlier claim that the illegal argument makes 6.8 answer 400, whereObjects.requireNonNullanswered 500, does not hold: 6.8 answers this script error 500 too.NaN/Infinityin a DOUBLE column). It is deferred to the typed-rows item.LAST_DAYover adate_nanoscolumn answers a DATE (ninth commit).date_nanostypesAny, so the arm conversion leaves LAST_DAY's receiver as it renders. In a query, LAST_DAY now reads such a receiver's runtime class, once per row (LastDayOfMonth.dateOf): aLocalDateas it is, aLocalDateTimeits date, a zoned value its UTC date. A nameless receiver is bound once.SQLTypeUtils.armSourceTypeunknown), and in LAST_DAY only. A typed receiver renders as before, byte for byte. An ingest processor and a transform keep their bytes.main's bytes (see "Recorded").On ES 6.8, a comparison of two date columns is scripted, as on the other majors (
cd85e6b1, fix(sql): a typed query answers one class per column, the type it reports #443): the hand-maintained ES 6 bridge gets the template's two-operandrequiresScript(ts < dfailed there withfailed to parse date field [d]);DateArithmeticSpec's 600 TIMESTAMP-vs-DATE comparisons now run on every major (the 72 column-against-column ones ran on 7 and up only): ES 6.8.23 600 / 600 equal DuckDB, 0 errors, the whole suite 30 / 30;es6bridge/test326 / 326; CI lint line green on a fresh clone.Query context only. An ingest processor and a transform keep their bytes, so a pipeline does not churn on
ALTER. A type is context-free, and date arithmetic andGREATEST/LEASTread it. So while a processor or transform script renders, a conditional andLAST_DAYanswer the type they answered before. This is scoped at the three render entry points bySQLTypeUtils.renderingIn. Without it, 500 processor renders of a 13,000-statement corpus moved, some to a wrong answer. With it, none moves.What changes for the user
CASE,COALESCE,NULLIF,GREATESTorLEASTover dates answers its reported type in every row. A date function over it no longer fails on half the rows.LAST_DAYanswers a DATE, without the time of day.NULLIF(x, NULL),COALESCE(…, NULL),LEAST(NULL, …)andGREATEST(NULL, …)compile and answer DuckDB's value.CAST(<keyword> AS TIMESTAMP)reads2024-01-03as2024-01-03T00:00:00Z.CAST(<a DATE> AS BIGINT)answers the epoch milliseconds of its UTC midnight, andCAST(<a DATE> AS TIME)00:00:00. A DATE column answers as before.Dependency
Based on
mainafter #439 to #444 merged; the rebase was clean. Before this push, the date / conditional / client-error area specs ran once on ES 8.18.3 on the rebased tip:DateArithmeticSpec,DateFunctionExecutionSpec,ScrollCompletenessSpec,GroupByCompletenessSpecandClientErrorStatusSpec, 193 / 193. CI runs every major.A rejected search page says why its shards failed (core and every client)
Elasticsearch gives why a shard failed (
ASIN is undefined outside [-1,1]under a script'sruntime error) only underfailed_shards[i].reason.caused_by. The ES 8 / ES 9 client appended it; the REST clients did not, so on Elasticsearch 7 a streamed SELECT outside a function's domain answeredall shards failed: runtime erroronly. One function in core,ShardFailures.shardReason, now reads it for every client: the ES 6 / ES 7 REST clients append it to a rejected page's message, and the ES 8 / ES 9 clients use it in place of their own copy (their message is unchanged). The test of a function outside its domain asserts the reason on every major. An existing REST unit row expected the message without it; it now expects Elasticsearch's reason.The scroll test of a rejected page now uses a query every major refuses with a 400 (
value > 'not-a-number'on an integer field). It relied on a script error, and Elasticsearch 6.8 answers a Painless runtime error with a 500.Tests
ConditionalArmClassSpec(new, 11 rows):NullLiteralFoldSpec(new, 8 rows): each fold, the consumers' NULL guard, no NULL constant over the family, the SQL round trip, and an ingest processor's bytes.DateOnlyTimestampCastSpec(new): a keyword to TIMESTAMP and to DATETIME, a date-only and a date-time literal, the completed text against the formatter, and an ingest processor's bytes.DateCastMidnightSpec(new, 12 rows):CURRENT_DATE, a DATE literal, CASE, COALESCE, NULLIF, GREATEST, LEAST andLAST_DAY, each to BIGINT and to TIME: the rendered Painless and the reported type;main;DATE_ADDover a DATE converted, a TIMESTAMPDATE_ADDand aDATETIME_ADDkept on the TIMESTAMP conversion;2024-01-03is1704240000000and00:00);bdbb2727(the four commits' tip) 9 of the 12 fail; the DATE column, the JVM and the processor / transform rows pass.DateDocValueEmissionSpec: LAST_DAY overd,ts, adate_nanosoperand, and its reported DATE.Testkit
DateFunctionExecutionSpec, blockGATE-024 typed-path values, over a 4-document table and adate_nanos/ undeclared-date index. Each row asserts DuckDB 1.5.5's value for every document. On this path a script field comes back with the response parser's class, so a DATE is asserted as itsyyyy-MM-ddtext and a TIMESTAMP as the instant it denotes: a timestamp under a DATE label fails, and so does a date under a TIMESTAMP label.LAST_DAY;DATE_TRUNCover a CASE of a TIMESTAMP and a DATE;COALESCE(CAST(n AS DOUBLE), 2);NULLIF(d, NULL);DATETIME_ADD(LEAST(NULL, dn), INTERVAL 1 HOUR);ISNOTNULL(CAST(kd AS TIMESTAMP));LAST_DAY(ts);LAST_DAYover an undeclared date column;COALESCE,CASEand twoNULLIFto BIGINT,COALESCEto TIME,CAST(CAST(ts AS DATE) AS BIGINT | TIME), a DATE literal andCURRENT_DATEto BIGINT. It asserts the UTC-midnight epoch milliseconds and00:00:00, not DuckDB's values (DuckDB refuses the casts).mainand whose script changes. Their values are asserted; theEXTRACTone runs on ES 7 and later, because it fails on 6.8 before and after this change.Round 2 rows:
ConditionalArmClassSpec: LAST_DAY's guard over a nameless operand; the type a processor writes (SQLTypeUtils.processorType,fromScript) and an UPDATE's pipeline description.NullLiteralFoldSpec: a cast over a CASE (VARCHAR, TIMESTAMP) under one guard; its processor row names the recorded twin it pins (a script that cannot compile).DateOperandSpec(new): time-of-day parts over each DATE producer, sub-day intervals, the reported types, the COALESCE besideDATE_ADD, an ingest processor's bytes.MixedArmsRefusalSpec(new): each refused arm pair with DuckDB's reason, the accepted pairs.MathDomainSpec(new): each function's domain test and reason, an ingest processor's bytes.GATE-024 typed-path values, 6 new rows: a time of day of a DATE (6 statements), LAST_DAY inside its operand's guard (2), a cast over a CASE (2, the NULL row asserted as NULL, not the text), a COALESCE besideDATE_ADD(2), the refusals (2), the domain failures (4 + an in-domain control). All 6 are red at the round-1 tip on ES 8.18.3 and ES 6.8.23 and green at the tip.Round 3 rows:
MathDomainSpec: the throw is anIllegalArgumentException.DateDocValueEmissionSpec: LAST_DAY over adate_nanosoperand reads its runtime class; a nameless operand of unknown type is bound once; a typed receiver has no class read.GatewayApi.run, each of ASIN, ACOS, SQRT, LOG and LOG10, streamed (no LIMIT: the Community quota streams it, and the failure surfaces when the rows are read) and withLIMIT 10(a search:runanswers an ElasticFailure). It asserts the failure, the server's status where the client carries it (500 on ES 6, 400 on ES 7 and later; ES 7 and 9 not measured locally) and DuckDB's reason where the client carries it (the REST clients, ES 6 and 7; not the Java client, ES 8 and 9), as ES's reason, not the script's own literal. Each gap is alerted in the run (see the ES row below).BigDecimal), as the GREATEST / LEAST rows do: ES 8 answers it1.0(the response parser's class, the recorded typed-rows gap), ES 6.81. n2 (nodn) answers NULL, DuckDB 1.5.5's answer (last_day(NULL::TIMESTAMP_NS), run).Moved pins
DateDocValueEmissionSpecLAST_DAY(d),LAST_DAY(ts), and the receiver-method rowtoInstantandwith.ParameterIdentitySpecLAST_DAY(d) = dd. LAST_DAY'sLocalDateis promoted to meet the raw column.MixedTemporalComparisonSpecLAST_DAY(d) = d,LAST_DAY(d) > CAST(ts AS DATE)PredicateTransformSurvivalSpecLAST_DAY(d) = ts,LAST_DAY(d) = LAST_DAY(ts),COALESCE(d) = 'x'COALESCE(d)answers its DATE.NullIfOperandSpecNULLIF(CAST(n AS KEYWORD), 'x')PainlessTargetSpecCOALESCE(d, d2) > CAST('2024-01-01' AS DATE): 1 → 0ConditionalArmClassSpecLAST_DAY(CASE WHEN n > 0 THEN ts ELSE d END)ParameterIdentitySpecCASE WHEN DATE_ADD(d, INTERVAL 1 DAY) > ts THEN DATE_ADD(d, INTERVAL 1 DAY) ELSE d END(and its swap)d + 1 dayreports a DATE beside the DATEd, so the CASE answers a DATE, each arm converted once; the interval still folds onto its own parameter (round 2).SQLQuerySpec(both bridges)TRY_CAST(COALESCE(NULLIF(createdAt, …), CURRENT_DATE - INTERVAL 2 HOUR) AS BIGINT)LocalDate.minus(2, HOURS), which throws, swallowed into NULL by the TRY_CAST (round 2).SQLQuerySpec(both bridges)IllegalArgumentException(round 3).DateDocValueEmissionSpecdate_nanosoperandThe 5th commit moves no existing pin. No processor or ingest pin moved.
ConditionalArmClassSpec,NullLiteralFoldSpecandDateOnlyTimestampCastSpeceach pin an ingest processor's bytes as they are onmain.Results (round 3, tip
12019581, base5dd10fb9)requireNonNull→IllegalArgumentException), and 17 are LAST_DAY over adate_nanosoperand (LAST_DAY(dn)×5, over a CASE, DATE_TRUNC or GREATEST ofdn×12). ES 6.8 target: 80, the throw only (dnis a plaindatethere). Processor and transform: 0. No typed receiver moves.sql/testcore/testsoftclient4es8-sql-bridge/test/es6bridge/testDateFunctionExecutionSpecblockGATE-024 typed-path values, at9cb31963(the two testkit rows amended into their commits; no product byte changed)co.elastic.clients.elasticsearch._types.ElasticsearchException("[es/search] failed: [search_phase_execution_exception] all shards failed"), not an ElasticError: no status, no reason.LIMIT 10: ElasticFailure, status 400, message "Elasticsearch error during singleSearch: search_phase_execution_exception - all shards failed; runtime error": the reason is missing (it is thecaused_byof each shard failure, which the client does not read).org.elasticsearch.client.ResponseException(status lineHTTP/1.1 500), not an ElasticError: no status carried; the message carries ES's response, with the reason ("caused_by":{"type":"illegal_argument_exception","reason":"ASIN is undefined outside [-1,1]"}).LIMIT 10: ElasticFailure, status 500 (the server's:"status":500), the reason carried.9cb31963)sbt "+ compile"(2.12 + 2.13), on the same clone12019581; not re-run on9cb31963(testkit main sources changed: CI)Results (round 2, tip
6f70a933)43b9aa0b(the reviewer's tools, same day)HOUR(LAST_DAY(ts)), the cast over a CASE,LAST_DAY(CASE … ELSE NULL END)). The ORDER BY shapes fail on both trees (recorded).43b9aa0b; the UPDATE descriptions andfromScripttypes of the reviewer's 18 expressions identical to base6fea466b)sql/testsoftclient4es8-sql-bridge/test/es6bridge/testDateFunctionExecutionSpecsbt "+ compile", CI lint line on a fresh cloneResults (round 1)
sql/testsoftclient4es8-sql-bridge/testes6bridge/testGATE-024 typed-path valuesmain" row fails because 5 of its 11 statements answered another class than DuckDB's type onmain(the gate compared their values); the other 6 answered as now.bdbb2727(the four commits' tip)2024-01-15for a BIGINT), 2 fail the search (`CAST(CAST(ts AS DATE) AS BIGINTGATE-024 typed-path valuesbdbb2727community/testwith this core'ssqlclasses first on the classpathsbt "+ compile"(2.12 + 2.13)headerCheck scalafmtSbtCheck scalafmtCheck test:scalafmtCheck, caches cleared, on a clone of the committed bytesES 7, ES 9 and the ES 6 Jest client are left to CI. The extensions run was not repeated for the 5th commit.
Release-gate coverage (render census, 13,289 statements, query context)
ISO_DATE_TIMEparseLAST_DAYrenders aLocalDateLAST_DAY(dn), excluded)date_nanosarm underDATE_TRUNC. That arm's type is unknown (date_nanostypesAny), so the node keeps its bytes as onmain: two render two classes, and one renders one class but callstruncatedToon aLocalDate.CAST(DATE_TRUNC(CASE … dn …, MONTH) AS DATE), where the cast overDATE_TRUNC's TEMPORAL answers aZonedDateTime.date_nanosarm (kept as onmain) and one pre-existing CAST-over-CASE precedence cell. 24 percentile cells are outside these groups.CASEof a cast to DATE and a DATE literal) renders one class. On Elasticsearch the same arm pair, as a simple CASE, is the fix row "keep a DATE literal a DATE beside a cast to DATE".COALESCE,CASEand twoNULLIFto BIGINT,COALESCEto TIME:toInstant()on aLocalDate) andCAST(NULLIF(<DATE literal>, CAST(ts AS TIME)) AS TIME)(toLocalTime()on aLocalDate). That last one is converted now, but its comparison of a DATE with a TIME is unchanged; it was not run.LEAST/GREATESTover dates to BIGINT (4 shapes) andLEAST(CAST(d AS DATE), NULL)to TIME. The 5th commit gives them the base's values again.LocalDateon the base and at the tip:NULLIFof DATE literals orDATE_PARSEto BIGINT and TIME.CURRENT_DATEand noLAST_DAYunder these casts.DateCastMidnightSpecpins them.ISO_DATE_TIME.main) and 3,970 on ES 6.8 (535 right onmain). The 11 statements of the "right onmain" row are a sample of them.Open points for review
SQLTypeUtils.renderingIn) is a new mechanism (accepted). It is aDynamicVariableset at the render entry points:Identifier.painless,FunctionN.painless,Case.painlessandScriptProcessor.fromScript. While a processor or transform renders, a conditional's andLAST_DAY's type answer what they answered before, so processor bytes do not move. It is thread-local, andwithValuerestores the outer value on exit, including on a throw.CAST(<DATE> AS BIGINT)answers the epoch milliseconds of the value's UTC midnight, andCAST(<DATE> AS TIME)00:00:00, for every operand that renders a DATE.out, which the cast sets to the target.main's answers.CAST(d AS BIGINT)is the stored instant's epoch milliseconds, as Elasticsearch SQL casts a date to a long.CAST(d AS TIME)is that instant's UTC time of day,00:00:00for a date-only value; a tracked testkit row pins it ("a CASE over CAST(d AS TIME) and a TIME literal").DateCastMidnightSpec(12) and the testkit row "convert a DATE to BIGINT and TIME through its UTC midnight" (9 statements). The row is red onbdbb2727and green at the 5th commit on ES 8.18.3 and ES 6.8.23.CAST(d AS INT)andCAST(d AS DOUBLE)answer the raw doc value (pre-existing, silent).CAST(ts AS BIGINT)is unchanged.Recorded for the lead (not fixed here)
date_nanosarms keepmain's bytes (residual, ruling 8).dnis a plaindate; the cast overDATE_TRUNC's TEMPORAL answers aZonedDateTimeunder DATE.LAST_DAY(dn)on ES 8 (LS-5-DN, 3 cells) answers a DATE at round 3 (ninth commit). The conditional arms above are not covered.NaNorInfinityreads the text, not a Double. The domain functions no longer produce one; other expressions still can.LAST_DAY(GREATEST(NULL, dn))has no NULL guard (pre-existing). The foldedGREATESTleavesdnunguarded, so a document withoutdnfails the statement, before and after the ninth commit (rendered, not run).main(6 shapes × P/C × SEL/GBK). 28 on ES 6.8 at this tip (24 onmain), the 7th shape beingCAST(CASE … dn ELSE DATE_PARSE(kd, '%Y-%m-%d') END AS TIME).CAST(CASE … CAST(d AS DATETIME) ELSE <DATE literal> END AS DATE)), 33920, 34365, 34404, 35341, 35374; ES 6.8 also 34408.leastCommonSuperType, GREATEST / LEAST throughnumericType(the PostgreSQL rule). They can part for a NUMERIC beside a whole number.LocalDateTime, while a DATETIME doc value or arm is aZonedDateTime.ISO_DATE_TIME.CAST(DATE_FORMAT(CAST(d AS DATE), '%Y') AS BIGINT)renders.toInstant().toEpochMilli()on a String, andCAST(YEAR(CAST(ts AS DATE)) AS BIGINT)the same on a number. The 5th commit does not change them.Release notes
CASE,COALESCE,NULLIF,GREATESTandLEASTanswer one class per column: the type they report, DuckDB's common type of their arms. Arms with no common type behave as before.LAST_DAYanswers a DATE, adate_nanosoperand included; it reported its operand's type before.NULLIF,COALESCE,LEASTorGREATESTis folded. Such statements compile and answer NULL-skipping values.CAST(<string> AS TIMESTAMP | DATETIME)reads a date-only value as its midnight UTC.CAST(<DATE> AS BIGINT)answers the epoch milliseconds of its UTC midnight, andCAST(<DATE> AS TIME)00:00:00, for a cast to DATE,CURRENT_DATE, a DATE literal,LAST_DAYand a DATE conditional. A DATE column answers as before.LAST_DAY(now DATE);DATE_ADD(COALESCE(d, d2), INTERVAL 1 DAY)TIMESTAMP -> DATE;DATE_ADD/DATE_SUBof whole days over a DATE (DATE_ADD(d, INTERVAL 1 DAY): TIMESTAMP -> DATE), and a sub-dayDATETIME_ADDover a DATE (DATETIME_ADD(GREATEST(<DATE>, <DATE>), INTERVAL 1 HOUR),DATETIME_ADD(CAST(ts AS DATE), INTERVAL 1 HOUR): DATE -> TIMESTAMP). A materialized view computing one declares the new type; DATE and TIMESTAMP map to the samedatefield.EXTRACTof a DATE answer 0 (its UTC midnight);HOUR(CAST(d AS DATE))failed onmain. A sub-day interval over a DATE answers its TIMESTAMP.NaN/-Infinity.🤖 Generated with Claude Code