Class SchemaReconciliation
Supports three strategies:
FIRST_FILE_WINS— use the first file's schema (existing behavior, no reconciliation)STRICT— validate all files share the exact same schemaUNION_BY_NAME— merge schemas by column name with safe type widening
Type widening is intentionally conservative: only lossless promotions are allowed.
This is NOT EsqlDataTypeConverter.commonType(), which allows LONG→DOUBLE (lossy above 2^53).
Under UNION_BY_NAME, any pair the lossless table cannot widen falls back to
DataType.KEYWORD (the cross-type join): values from numerically-typed files are stringified
via ColumnMapping's per-block cast and a single response Warning header per
affected column tells the user what happened. This matches the industry baseline (DuckDB,
ClickHouse, Spark all widen to string as the cross-type floor) and turns "samplers disagreed"
— the normal steady state for sampling-based readers — from a hard error into a benign
widening. Users who want the strict-mismatch error can opt into schema_resolution =
"strict" which still throws.
The lossy LONG + DOUBLE pair is *not* covered by the lossless table on purpose
(precision loss above 2^53). Under UBN it goes to KEYWORD, which is louder and safer
than silent precision loss; the lossless table itself stays unchanged.
The four schemas in an external-source query
Four distinct schemas exist in every external-source query. In simpler modes (single file, FFW, STRICT) some collapse onto each other; under UNION_BY_NAME all four are genuinely distinct. Code touchingFileSplit.readSchema(), ExternalSourceExec.attributes,
or ColumnMapping reads much more clearly with these names in mind:
- File schema (per-file, file shape)
- What's literally in one file. Parquet/ORC: read from the file footer. CSV/NDJSON:
inferred from a byte sample. Carried per-file on
FileSplit.readSchema(). - Unified schema (one for the whole table)
- The cross-file harmonized schema. Produced here as
SchemaReconciliation.Result.unifiedSchema(): FFW takes the anchor file's schema, STRICT validates a common schema, UBN takes the column-name union with type widening. BecomesExternalSourceExec.attributesat first, before the optimizer's projection pruning rewrites that field. - Query schema (unified shape; same for every file in the query)
- The subset of unified schema the query actually materializes after projection pruning.
Lives on
ExternalSourceExec.attributeson the wire. Drives the per-fileColumnMappingafterColumnMapping.pruneToPerFileQuery(org.elasticsearch.xpack.esql.datasources.ExternalSchema, org.elasticsearch.xpack.esql.datasources.ExternalSchema, org.elasticsearch.xpack.esql.datasources.ExternalSchema). - Per-file query schema (per-file, file shape — what the reader actually produces)
Query schema∩ this file's columns, ordered to match the file's natural layout. Derived per file at split-construction time and at read time. Under FFW and STRICT it collapses to the Query schema because every file has every projected column.
Worked example (UNION_BY_NAME)
a.csv = [name:keyword, age:int]
b.csv = [age:long, name:keyword, city:keyword]
query: EXTERNAL "*.csv" WITH {"schema_resolution": "union_by_name"}
| KEEP name, city
| SORT name
File schema: a → [name:keyword, age:int]
b → [age:long, name:keyword, city:keyword]
Unified schema: [name:keyword, age:long, city:keyword] (age widens int → long)
Query schema: [name:keyword, city:keyword] (KEEP drops age)
Per-file query schema: a → [name] (no city in a)
b → [name, city] (in b's natural order)
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final recordPer-file schema information collected during reconciliation.static final recordResult of schema reconciliation during planning. -
Method Summary
Modifier and TypeMethodDescriptionstatic SchemaReconciliation.ResultreconcileStrict(StoragePath referenceFile, Map<StoragePath, SourceMetadata> fileMetadata) STRICT reconciliation: validate all files share the exact same schema.static SchemaReconciliation.ResultreconcileUnionByName(Map<StoragePath, SourceMetadata> fileMetadata) UNION_BY_NAME reconciliation: merge schemas from all files into a superset.static DataTypeschemaWiden(DataType a, DataType b) Safe type widening for schema reconciliation.
-
Method Details
-
schemaWiden
Safe type widening for schema reconciliation. Only lossless promotions are allowed; returnsnullif no safe supertype exists.Widening rules:
- INTEGER + LONG → LONG (lossless: int32 ⊆ int64)
- INTEGER + DOUBLE → DOUBLE (lossless: int32 ≤ 2^31 < 2^53)
- DATETIME + DATE_NANOS → DATE_NANOS (more precise type wins)
DataType.KEYWORDfor those — seewidenToCommonOrKeyword(org.elasticsearch.xpack.esql.core.type.DataType, org.elasticsearch.xpack.esql.core.type.DataType)andreconcileUnionByName(java.util.Map<org.elasticsearch.xpack.esql.datasources.spi.StoragePath, org.elasticsearch.xpack.esql.datasources.spi.SourceMetadata>). LONG + DOUBLE deliberately stays out of this table (precision loss above 2^53) and is therefore one of the pairs that goes toKEYWORDunder UBN.- Returns:
- the widened type, or null if no safe supertype exists
-
reconcileStrict
public static SchemaReconciliation.Result reconcileStrict(StoragePath referenceFile, Map<StoragePath, SourceMetadata> fileMetadata) STRICT reconciliation: validate all files share the exact same schema. Nullability differences are tolerated; all other differences produce an error.- Parameters:
referenceFile- path of the first (reference) filefileMetadata- ordered map of file path → metadata (first entry is the reference)- Returns:
- reconciliation result with the reference schema and per-file info
- Throws:
IllegalArgumentException- if any file's schema doesn't match
-
reconcileUnionByName
public static SchemaReconciliation.Result reconcileUnionByName(Map<StoragePath, SourceMetadata> fileMetadata) UNION_BY_NAME reconciliation: merge schemas from all files into a superset. Missing columns are NULL-filled; type differences are resolved by safe widening or, when no lossless supertype exists, by falling back toDataType.KEYWORDwith a per-columnWarningresponse header. See the class javadoc for the rationale and the lattice picture.- Parameters:
fileMetadata- ordered map of file path → metadata (insertion order = file sort order)- Returns:
- reconciliation result with unified schema and per-file mappings
-