Class DeclaredTypeCoercions
castBlock(org.elasticsearch.compute.data.Block, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.common.time.DateFormatter, org.elasticsearch.compute.data.BlockFactory, java.lang.String, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings)), and the one scalar
conversion the string → date pair uses everywhere (parseDatetimeMillis(java.lang.String, org.elasticsearch.common.time.DateFormatter)).
The one concept: reading a file IS ingesting it
A dataset mapping may declare a column type that differs from the type physically in the file (Hive/Trino-style: the declaration is the table schema, readers coerce toward it). Reading a file value against a declared column is the same operation as indexing a document field against a mapping, so the coercion authority is the field mappers' lenient index-time coercion ("123" → long, long → double,
string → datetime via the column's declared format, …),
not ES|QL's query-cast rules. Once a value has been coerced into the declared shape it is an
ordinary ES|QL value and query-layer conversions (::, TO_*) apply downstream
as usual.
What is coercible — the mapper coercion set
supports(org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType) follows what the field mappers accept at ingest, deviating on the safe
(reject) side where a mapper is more permissive than a reader can be faithful to — the date
mapper's default format also parses numeric and fractional tokens (epoch_millis halves
a 1.5 token down to a truncated instant), while supports(DOUBLE, DATETIME) is
deliberately false because a fractional value has no unambiguous epoch reading:
- whole-number targets (
integer/long): any numeric or string source, reusing the ES|QL::cast engine (numericCoercer(org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType)) so a declared read is value-identical to an explicit::long/::integer— numeric strings parse (fractional and scientific accepted), the result rounds (not truncates), out-of-range throwsInvalidArgumentException.unsigned_longkeeps its::-faithfulcoerceToUnsignedLong(java.lang.Object)twin (truncates toward zero, matching::unsigned_long);doubleparses withDouble.parseDoubleand returns the IEEE value as-is (NaN/Infinitypass through, matching the native columnar double read and CSV — an external read preserves the file's value; the mapper's finite-only rule is an index-time concern, not a read one); - string targets (
keyword/text): any decodable scalar source — ingest stringifies the token (temporal sources render in the ISO form the default date format parses back; ip sources render as address text, never the encoded bytes). The source set is closed over exactly the types the readers can decode a block of (string, whole-number, double, boolean, temporal, ip) so a pairsupportsadmits can never reach a value reader that has no arm for it; boolean: string sources only, parsed strictly and case-insensitively (strictParseBoolean(java.lang.String): onlytrue/falsein any case; every other token fails loudly). This deliberately diverges from::boolean, which maps a non-truetoken silently tofalse— a silent wrong answer this read must not introduce;datetime: string sources parse viaparseDatetimeMillis(java.lang.String, org.elasticsearch.common.time.DateFormatter)with the column's declaredformat(else the ISO default), whole-number sources reinterpret as epoch milliseconds (theepoch_millishalf of the default date format);date_nanos: string sources parse ISO,datetimesources widen millis→nanos (what an epoch-millis token ingests to in adate_nanosfield; out-of-nanos-range instants fail per value). Plain whole numbers stay out — a raw long is ambiguous between a millis and a nanos payload;ip: string sources only, parsed with the same underlying primitive the ip mapper delegates to (InetAddressesparse + the 16-byte doc-values encoding).
NULL/UNSUPPORTED physical columns support nothing (the readers cannot decode a
value to coerce). An unsupported pair is rejected at resolution with an actionable error;
there is no third state — a declared type that cannot be produced from the physical column
must never silently read as null.
Where the conversion runs — same predicate, different timing
- Columnar formats (Parquet, ORC) know the physical type upfront from the file
footer, so
ExternalSourceResolverrunssupports(org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType)once at resolution and fails fast. The readers fuse a handful of pairs directly into their decode loops (fusedInDecode(org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType)); every other supported pair decodes the column at the file's own type and coerces it withcastBlock(org.elasticsearch.compute.data.Block, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.common.time.DateFormatter, org.elasticsearch.compute.data.BlockFactory, java.lang.String, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings). Per-value failures (numeric overflow, an unparseable token) follow the read'sErrorPolicythe same way the text readers' parse failures do — the default (null_field;skip_rowdegrades to it, a columnar batch cannot drop one row) nulls the cell and emits a responseWarningheader,ignore_malformed-style, whilefail_fastfails the read on the first bad value. Fused arms andcastBlock(org.elasticsearch.compute.data.Block, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.common.time.DateFormatter, org.elasticsearch.compute.data.BlockFactory, java.lang.String, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings)route the failure through the oneonCoercionFailure(java.lang.String, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, java.lang.RuntimeException, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings)chokepoint so the two paths cannot disagree. Readers also re-checksupports(org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType)per file for a declared column, since a multi-file glob can drift from the anchor footer; an inferred column may only widen, so a drifted inferred type null-fills rather than taking this lossy escape (never narrows). - Text formats (CSV/TSV, NDJSON) have no physical schema — every value is a string,
so the parse into the declared type is the coercion and a bad token follows the
reader's own per-value error policy. Their declared date
formatparse goes through the sameparseDatetimeMillis(java.lang.String, org.elasticsearch.common.time.DateFormatter)scalar as the columnar string→date coercion, so the same token with the same declared format produces the same instant regardless of which format carried it.
TODO: the columnar-vs-text classification this predicate pairs with
(ExternalSourceResolver.FILE_TYPED_FORMATS) has a standing TODO to move onto the
FormatReader SPI as a capability; if that happens, per-format coercion support belongs
on the same capability surface.
-
Method Summary
Modifier and TypeMethodDescriptionstatic BlockcastBlock(Block source, DataType from, DataType to, DateFormatter declaredFormat, BlockFactory blockFactory, String columnName, SkipWarnings warnings) Coerces a decoded physical-type block into a declared-type block, value by value, with the field mappers' ingest coercion (see the class Javadoc).static booleanfusedInDecode(DataType from, DataType to) The coercible pairs the columnar decode loops implement directly (fused into the decode, nocastBlock(org.elasticsearch.compute.data.Block, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.common.time.DateFormatter, org.elasticsearch.compute.data.BlockFactory, java.lang.String, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings)pass): the losslessinteger → longwiden, thelong → datetimeepoch-millis reinterpret, thestring → datetimeparse with the column's declaredformat, and thekeyword ↔ textrelabel (same bytes).static voidonCoercionFailure(String columnName, DataType from, DataType to, RuntimeException e, SkipWarnings warnings) The one coercion-failure chokepoint, shared bycastBlock(org.elasticsearch.compute.data.Block, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.common.time.DateFormatter, org.elasticsearch.compute.data.BlockFactory, java.lang.String, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings)and the readers' fused decode arms so a failed value behaves identically whichever path decoded it: with anullwarningssink (strict,error_mode: fail_fast) the failure propagates and the read fails; with a live sink the caller nulls the cell/position and one capped responseWarningheader records the reason.static longparseDatetimeMillis(String value, DateFormatter format) The one string→datetime conversion for declared date columns, shared by every reader: the text readers' declared-formatparse (CSV/TSV, NDJSON) and the columnar readers' string→datetime coercion (Parquet, ORC) all route here, so identical input bytes with an identical declared format produce the identical epoch instant regardless of file format.static booleanstrictParseBoolean(String value) Strict, case-insensitive boolean parse for a declaredbooleanread.static booleanWhether an external reader can coerce a value physically stored asfrominto the declared typetoat read time: exactly the pairs the field mappers coerce at ingest (see the class Javadoc for the set).
-
Method Details
-
supports
Whether an external reader can coerce a value physically stored asfrominto the declared typetoat read time: exactly the pairs the field mappers coerce at ingest (see the class Javadoc for the set). Equal types trivially returntrue;NULLandUNSUPPORTEDalways returnfalse(the readers cannot decode such a column, so there is no value to coerce). This is THE castability predicate: resolution-time rejects consult it directly; the reader-side per-file null-fill validation consults it only for a declared column (an inferred cross-file clash widens-or-nulls, never narrows — seeParquetFormatReader.validatePlannerTypesAgainstFile), so a lossy narrowing is admitted exactly where a declaration licenses it. -
fusedInDecode
The coercible pairs the columnar decode loops implement directly (fused into the decode, nocastBlock(org.elasticsearch.compute.data.Block, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.common.time.DateFormatter, org.elasticsearch.compute.data.BlockFactory, java.lang.String, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings)pass): the losslessinteger → longwiden, thelong → datetimeepoch-millis reinterpret, thestring → datetimeparse with the column's declaredformat, and thekeyword ↔ textrelabel (same bytes). Every othersupportedpair decodes at the file's own type and coerces throughcastBlock(org.elasticsearch.compute.data.Block, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.common.time.DateFormatter, org.elasticsearch.compute.data.BlockFactory, java.lang.String, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings). Both Parquet and ORC consult this so the two readers cannot disagree about which path a pair takes. -
castBlock
public static Block castBlock(Block source, DataType from, DataType to, @Nullable DateFormatter declaredFormat, BlockFactory blockFactory, @Nullable String columnName, @Nullable SkipWarnings warnings) Coerces a decoded physical-type block into a declared-type block, value by value, with the field mappers' ingest coercion (see the class Javadoc). Preserves nulls and multi-value positions. Does NOT take ownership ofsource; the caller closes it. The returned block is a fresh reference the caller owns (for the trivialfrom == tocase the source is ref-bumped and returned).Per-value failures — numeric overflow, an unparseable token — follow the bulk API's lenient model when
warningsis non-null: the whole position is nulled and one capped responseWarningheader records the reason (never a hard read failure, never a silent wrong value). With anullwarningssink the coercion is strict and the failure propagates to the caller.- Parameters:
declaredFormat- the column's declared date parse pattern for the string→datetime pair (null= the ISO default); ignored by every other paircolumnName- column name used in warning details; may benullwhen the caller is strict (warnings == null)
-
onCoercionFailure
public static void onCoercionFailure(@Nullable String columnName, DataType from, DataType to, RuntimeException e, @Nullable SkipWarnings warnings) The one coercion-failure chokepoint, shared bycastBlock(org.elasticsearch.compute.data.Block, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.common.time.DateFormatter, org.elasticsearch.compute.data.BlockFactory, java.lang.String, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings)and the readers' fused decode arms so a failed value behaves identically whichever path decoded it: with anullwarningssink (strict,error_mode: fail_fast) the failure propagates and the read fails; with a live sink the caller nulls the cell/position and one capped responseWarningheader records the reason. Callers append the null themselves — this method only decides throw-vs-warn. -
strictParseBoolean
Strict, case-insensitive boolean parse for a declaredbooleanread. Accepts onlytrue/false(any case) and fails every other token throughonCoercionFailure(java.lang.String, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, java.lang.RuntimeException, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings)(warn+null or fail-fast). This deliberately diverges from::boolean(EsqlDataTypeConverter.stringToBoolean(java.lang.String), which maps every non-truetoken —"yes","1", a typo — silently tofalse): a silentfalseon a bad boolean token is exactly the wrong-answer class this feature must not introduce, so the read-time coercion rejects the token loudly instead. The numeric arms still reuse::verbatim; only boolean is stricter, and by design. -
parseDatetimeMillis
The one string→datetime conversion for declared date columns, shared by every reader: the text readers' declared-formatparse (CSV/TSV, NDJSON) and the columnar readers' string→datetime coercion (Parquet, ORC) all route here, so identical input bytes with an identical declared format produce the identical epoch instant regardless of file format.The format may be a column's declared
formator a reader's file-leveldatetime_format— this is the shared string→datetime conversion, not a declared-only one.With a format the parse is strict and zone-aware (
DateFormatter.parseMillis(java.lang.String)defaults a missing zone to UTC) — the same parse the date field mapper runs against its mapping'sformatat ingest; without one it falls back toEsqlDataTypeConverter.dateTimeToLong(String)— ISO (strict_date_optional_time) semantics. ThrowsIllegalArgumentExceptionon an unparseable value; callers decide whether that is a per-row error (text error policy), a nulled cell with a response Warning (castBlock(org.elasticsearch.compute.data.Block, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.common.time.DateFormatter, org.elasticsearch.compute.data.BlockFactory, java.lang.String, org.elasticsearch.xpack.esql.datasources.spi.SkipWarnings)with a warnings sink), or a hard failure.
-