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)), the block shape a
declared type reads into (elementTypeFor(org.elasticsearch.xpack.esql.core.type.DataType)/builderFor(org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.compute.data.BlockFactory, int), which every text and
columnar reader consults instead of re-deriving it), 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 Block.BuilderbuilderFor(DataType to, BlockFactory blockFactory, int positions) The block builder for a declared column's target type, derived fromelementTypeFor(org.elasticsearch.xpack.esql.core.type.DataType)so the builder and theElementTypea reader projects can never disagree.static 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 longcoerceToUnsignedLong(Object value) Theunsigned_longtwin ofNumberType.parsewithcoerce=true: numeric strings parse, decimals truncate toward zero, out-of-[0, 2^64-1]-range throws.static ElementTypeelementTypeFor(DataType type) TheDataType→ElementTypeauthority for the declared-read path: every format reader that materializes a declared or projected column into a block resolves its shape here.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. -
coerceToUnsignedLong
Theunsigned_longtwin ofNumberType.parsewithcoerce=true: numeric strings parse, decimals truncate toward zero, out-of-[0, 2^64-1]-range throws. Returns the sign-flip block encoding (NumericUtils.asLongUnsigned(BigInteger)), matching the index path.Public for the same reason as
strictParseBoolean(java.lang.String)andparseDatetimeMillis(java.lang.String, org.elasticsearch.common.time.DateFormatter): the text readers (CSV/TSV, NDJSON) decode a token to aStringorNumberthemselves and then delegate the declared-type conversion here, so a declaredunsigned_longproduces the identical block encoding regardless of file format. -
elementTypeFor
TheDataType→ElementTypeauthority for the declared-read path: every format reader that materializes a declared or projected column into a block resolves its shape here. The enumeration is not repeated: it is delegated toPlannerUtils.toElementType(org.elasticsearch.xpack.esql.core.type.DataType), the engine-wide mapping, so the declared-read path holds no secondDataTypeswitch over block shape. What this method adds is the declared-read guard, expressed on theElementTypeaxis rather than as a secondDataTypeenumeration: the scalar shapes this SPI'svalueWriter(org.elasticsearch.compute.data.Block.Builder, org.elasticsearch.xpack.esql.core.type.DataType)knows how to append, plusElementType.NULL.Expressing the guard on the shape axis is what keeps the readers honest. A future declarable type inherits its block shape automatically, while a
DOC/COMPOSITE/AGGREGATE_METRIC_DOUBLE-shaped type still fails loudly at page setup instead of silently building the wrong block. Format readers must call this rather than re-deriving the mapping locally — the reader-local copies are exactly howunsigned_longcame to be accepted at dataset-create time and then unreadable at query time.- Throws:
IllegalArgumentException- if the type has no block shape this SPI can write. This is the only exception this method throws:PlannerUtils.toElementType(org.elasticsearch.xpack.esql.core.type.DataType)signals its own unmappable types with anEsqlIllegalArgumentException, which is not anIllegalArgumentException, so it is remapped here. Callers therefore need one catch clause, and the contract matches the per-reader mappings this method replaced.
-
builderFor
The block builder for a declared column's target type, derived fromelementTypeFor(org.elasticsearch.xpack.esql.core.type.DataType)so the builder and theElementTypea reader projects can never disagree. -
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.
-