Skip to content

Commit 3b99762

Browse files
implicit casting for date and date_nanos in EsRelation
1 parent 4284c16 commit 3b99762

File tree

4 files changed

+155
-78
lines changed

4 files changed

+155
-78
lines changed

x-pack/plugin/esql/qa/testFixtures/src/main/resources/union_types.csv-spec

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1998,14 +1998,14 @@ FROM date_nanos, date_nanos_union_types
19981998
| LIMIT 4
19991999
;
20002000

2001-
warning:Line 2:13: evaluation of [nanos] failed, treating result as null. Only first 20 failures recorded.
2002-
warning:Line 2:13: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
2001+
warning:Line 1:1: evaluation of [FROM date_nanos, date_nanos_union_types] failed, treating result as null. Only first 20 failures recorded.
2002+
warning:Line 1:1: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
20032003

2004-
num:long | nanos:date_nanos | millis:date_nanos
2005-
0 | 2023-01-23T13:55:01.543Z | 1999-10-23T12:15:03.360103847Z
2006-
0 | 2023-01-23T13:55:01.543123456Z | 1999-10-23T12:15:03.360Z
2007-
0 | 2023-02-23T13:33:34.937Z | 1999-10-23T12:15:03.360103847Z
2008-
0 | 2023-02-23T13:33:34.937193Z | 1999-10-23T12:15:03.360Z
2004+
millis:date_nanos | nanos:date_nanos | num:long
2005+
1999-10-23T12:15:03.360103847Z | 2023-01-23T13:55:01.543Z | 0
2006+
1999-10-23T12:15:03.360Z | 2023-01-23T13:55:01.543123456Z | 0
2007+
1999-10-23T12:15:03.360103847Z | 2023-02-23T13:33:34.937Z | 0
2008+
1999-10-23T12:15:03.360Z | 2023-02-23T13:33:34.937193Z | 0
20092009
;
20102010

20112011
ImplicitCastingMultiTypedMVFieldsEval
@@ -2018,13 +2018,13 @@ FROM date_nanos, date_nanos_union_types
20182018
| LIMIT 4
20192019
;
20202020

2021-
warning:Line 2:23: evaluation of [nanos] failed, treating result as null. Only first 20 failures recorded.
2022-
warning:Line 2:23: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
2023-
num:long | nanos:date_nanos | millis:date_nanos
2024-
0 | 2023-03-23T12:15:03.360Z | 1999-10-22T12:15:03.360103847Z
2025-
0 | 2023-03-23T12:15:03.360Z | 1999-10-23T12:15:03.360103847Z
2026-
0 | 2023-03-23T12:15:03.360103847Z | 1999-10-22T12:15:03.360Z
2027-
0 | 2023-03-23T12:15:03.360103847Z | 1999-10-23T12:15:03.360Z
2021+
warning:Line 1:1: evaluation of [FROM date_nanos, date_nanos_union_types] failed, treating result as null. Only first 20 failures recorded.
2022+
warning:Line 1:1: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
2023+
millis:date_nanos | num:long | nanos:date_nanos
2024+
1999-10-22T12:15:03.360103847Z | 0 | 2023-03-23T12:15:03.360Z
2025+
1999-10-23T12:15:03.360103847Z | 0 | 2023-03-23T12:15:03.360Z
2026+
1999-10-22T12:15:03.360Z | 0 | 2023-03-23T12:15:03.360103847Z
2027+
1999-10-23T12:15:03.360Z | 0 | 2023-03-23T12:15:03.360103847Z
20282028
;
20292029

20302030
ImplicitCastingMultiTypedMVFieldsWhere
@@ -2037,8 +2037,8 @@ FROM date_nanos, date_nanos_union_types
20372037
| SORT millis
20382038
;
20392039

2040-
warning:Line 2:52: evaluation of [nanos] failed, treating result as null. Only first 20 failures recorded.
2041-
warning:Line 2:52: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
2040+
warning:Line 1:1: evaluation of [FROM date_nanos, date_nanos_union_types] failed, treating result as null. Only first 20 failures recorded.
2041+
warning:Line 1:1: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
20422042
millis:date_nanos | nanos:date_nanos | num:long
20432043
2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z | 1698063303360103847
20442044
2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z | 1698063303360103847
@@ -2056,8 +2056,8 @@ FROM date_nanos, date_nanos_union_types
20562056
| STATS max = MAX(millis), min = MIN(nanos)
20572057
;
20582058

2059-
warning:Line 2:38: evaluation of [nanos] failed, treating result as null. Only first 20 failures recorded.
2060-
warning:Line 2:38: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
2059+
warning:Line 1:1: evaluation of [FROM date_nanos, date_nanos_union_types] failed, treating result as null. Only first 20 failures recorded.
2060+
warning:Line 1:1: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
20612061

20622062
max:date_nanos | min:date_nanos
20632063
2023-10-23T13:55:01.543123456Z | 2023-01-23T13:55:01.543Z
@@ -2067,12 +2067,12 @@ ImplicitCastingMultiTypedMVFieldsStatsValues
20672067
required_capability: date_nanos_type
20682068
required_capability: implicit_casting_date_and_date_nanos
20692069

2070-
FROM date_nanos, date_nanos_union_types
2070+
FROM date_nanos, date_nanos_union_types
20712071
| STATS c = MV_COUNT(VALUES(nanos))
20722072
;
20732073

2074-
warning:Line 2:29: evaluation of [nanos] failed, treating result as null. Only first 20 failures recorded.
2075-
warning:Line 2:29: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
2074+
warning:Line 1:1: evaluation of [FROM date_nanos, date_nanos_union_types] failed, treating result as null. Only first 20 failures recorded.
2075+
warning:Line 1:1: java.lang.IllegalArgumentException: milliSeconds [-1457696696640] are before the epoch in 1970 and cannot be converted to nanoseconds
20762076

20772077
c:integer
20782078
19

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java

Lines changed: 120 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -177,20 +177,13 @@ public class Analyzer extends ParameterizedRuleExecutor<LogicalPlan, AnalyzerCon
177177
new ResolveEnrich(),
178178
new ResolveInference(),
179179
new ResolveLookupTables(),
180-
new ResolveFunctions()
180+
new ResolveFunctions(),
181+
new ResolveUnionTypesInEsRelation()
181182
),
182-
new Batch<>(
183-
"Resolution",
184-
/*
185-
* ImplicitCasting must be before ResolveRefs. Because a reference is created for a Bucket in Aggregate's aggregates,
186-
* resolving this reference before implicit casting may cause this reference to have customMessage=true, it prevents further
187-
* attempts to resolve this reference.
188-
*/
189-
new ImplicitCasting(),
190-
new ResolveRefs(),
191-
new ResolveUnionTypes(), // Must be after ResolveRefs, so union types can be found
192-
// Must be after ResolveUnionTypes, if there is explicit casting on the union typed fields, implicit casting won't be added
193-
new ImplicitCastingForUnionTypedFields()
183+
new Batch<>("Resolution", new ResolveRefs(), new ImplicitCasting(), new ResolveUnionTypes() // Must be after ResolveRefs, so union
184+
// types can be found
185+
// Must be after ResolveUnionTypes, if there is explicit casting on the union typed fields, implicit casting won't be added
186+
// new ImplicitCastingForUnionTypedFields()
194187
),
195188
new Batch<>("Finish Analysis", Limiter.ONCE, new AddImplicitLimit(), new AddImplicitForkLimit(), new UnionTypesCleanup())
196189
);
@@ -584,7 +577,7 @@ private Aggregate resolveAggregate(Aggregate aggregate, List<Attribute> children
584577
}
585578
}
586579

587-
if (Resolvables.resolved(groupings) == false || (Resolvables.resolved(aggregates) == false)) {
580+
if (Resolvables.resolved(groupings) == false || Resolvables.resolved(aggregates) == false) {
588581
ArrayList<Attribute> resolved = new ArrayList<>();
589582
for (Expression e : groupings) {
590583
Attribute attr = Expressions.attribute(e);
@@ -595,17 +588,29 @@ private Aggregate resolveAggregate(Aggregate aggregate, List<Attribute> children
595588
List<Attribute> resolvedList = NamedExpressions.mergeOutputAttributes(resolved, childrenOutput);
596589

597590
List<NamedExpression> newAggregates = new ArrayList<>();
598-
for (NamedExpression ag : aggregate.aggregates()) {
599-
var agg = (NamedExpression) ag.transformUp(UnresolvedAttribute.class, ua -> {
600-
Expression ne = ua;
601-
Attribute maybeResolved = maybeResolveAttribute(ua, resolvedList);
602-
if (maybeResolved != null) {
603-
changed.set(true);
604-
ne = maybeResolved;
605-
}
606-
return ne;
607-
});
608-
newAggregates.add(agg);
591+
// If the groupings are not resolved, skip the resolution of the references to groupings in the aggregates, resolve the
592+
// aggregations that do not reference to groupings, so that the fields/attributes referenced by the aggregations can be
593+
// resolved, and verifier doesn't report field/reference/column not found errors for them.
594+
boolean groupingResolved = Resolvables.resolved(groupings);
595+
int size = groupingResolved ? aggregates.size() : aggregates.size() - groupings.size();
596+
for (int i = 0; i < aggregates.size(); i++) {
597+
NamedExpression maybeResolvedAgg = aggregates.get(i);
598+
if (i < size) { // Skip resolving references to groupings in the aggregations if the groupings are not resolved yet.
599+
maybeResolvedAgg = (NamedExpression) maybeResolvedAgg.transformUp(UnresolvedAttribute.class, ua -> {
600+
Expression ne = ua;
601+
Attribute maybeResolved = maybeResolveAttribute(ua, resolvedList);
602+
// An item in aggregations can reference to groupings explicitly, if groupings are not resolved yet and
603+
// maybeResolved is not resolved, return the original UnresolvedAttribute, so that it has another chance
604+
// to get resolved in the next iteration.
605+
// For example STATS c = count(emp_no), x = d::int + 1 BY d = (date == "2025-01-01")
606+
if (groupingResolved || maybeResolved.resolved()) {
607+
changed.set(true);
608+
ne = maybeResolved;
609+
}
610+
return ne;
611+
});
612+
}
613+
newAggregates.add(maybeResolvedAgg);
609614
}
610615

611616
// TODO: remove this when Stats interface is removed
@@ -1598,7 +1603,7 @@ public LogicalPlan apply(LogicalPlan plan) {
15981603
// Collect field attributes from previous runs
15991604
plan.forEachUp(EsRelation.class, rel -> {
16001605
for (Attribute attr : rel.output()) {
1601-
if (attr instanceof FieldAttribute fa && fa.field() instanceof MultiTypeEsField) {
1606+
if (attr instanceof FieldAttribute fa && fa.field() instanceof MultiTypeEsField && fa.synthetic()) {
16021607
unionFieldAttributes.add(fa);
16031608
}
16041609
}
@@ -1674,11 +1679,58 @@ private Expression resolveConvertFunction(ConvertFunction convert, List<FieldAtt
16741679
var resolvedField = resolvedMultiTypeEsField(fa, typeResolutions);
16751680
return createIfDoesNotAlreadyExist(fa, resolvedField, unionFieldAttributes);
16761681
}
1677-
} else if (convert.field() instanceof AbstractConvertFunction subConvert) {
1678-
return convertExpression.replaceChildren(
1679-
Collections.singletonList(resolveConvertFunction(subConvert, unionFieldAttributes))
1680-
);
1681-
}
1682+
} else if (convert.field() instanceof FieldAttribute fa
1683+
&& fa.synthetic() == false
1684+
&& fa.field() instanceof MultiTypeEsField mtf) {
1685+
// This is an explicit casting of a union typed field that has been converted to MultiTypeEsField in EsRelation by
1686+
// ResolveUnionTypesInEsRelation, do not do double casting.
1687+
if (((Expression) convert).dataType() == mtf.getDataType()) {
1688+
// The same data type between implicit and explicit casting, explicit conversion is not needed
1689+
return convert.field();
1690+
} else {
1691+
// TODO Is there an easy way to convert from one MultiTypeEsField to another MultiTypeEsField?
1692+
HashMap<TypeResolutionKey, Expression> typeResolutions = new HashMap<>();
1693+
Set<DataType> supportedTypes = convert.supportedTypes();
1694+
Holder<Boolean> conversionSupported = new Holder<>(true);
1695+
// Get the type of each field in the multi typed field and check if the explicit conversion is supported
1696+
mtf.getIndexToConversionExpressions().forEach((indexName, conversionExpression) -> {
1697+
AbstractConvertFunction c = (AbstractConvertFunction) conversionExpression;
1698+
FieldAttribute fieldAttribute = (FieldAttribute) c.field();
1699+
DataType type = fieldAttribute.dataType();
1700+
if (supportedTypes.contains(type.widenSmallNumeric())) {
1701+
TypeResolutionKey key = new TypeResolutionKey(fa.name(), type);
1702+
var concreteConvert = typeSpecificConvert(convert, fa.source(), fieldAttribute.field());
1703+
typeResolutions.putIfAbsent(key, concreteConvert);
1704+
} else {
1705+
conversionSupported.set(false);
1706+
}
1707+
});
1708+
// If the conversions are supported, create a new FieldAttribute with a new MultiTypeEsField, and add it to
1709+
// unionFieldAttributes.
1710+
if (conversionSupported.get()) {
1711+
// build the map between index name and conversion expressions
1712+
Map<String, Expression> indexToConversionExpressions = new HashMap<>();
1713+
for (Map.Entry<String, Expression> entry : mtf.getIndexToConversionExpressions().entrySet()) {
1714+
String indexName = entry.getKey();
1715+
AbstractConvertFunction originalConversionFunction = (AbstractConvertFunction) entry.getValue();
1716+
TypeResolutionKey key = new TypeResolutionKey(fa.name(), originalConversionFunction.field().dataType());
1717+
Expression newConversionFunction = typeResolutions.get(key);
1718+
indexToConversionExpressions.put(indexName, newConversionFunction);
1719+
}
1720+
MultiTypeEsField multiTypeEsField = new MultiTypeEsField(
1721+
fa.name(),
1722+
((Expression) convert).dataType(),
1723+
false,
1724+
indexToConversionExpressions
1725+
);
1726+
return createIfDoesNotAlreadyExist(fa, multiTypeEsField, unionFieldAttributes);
1727+
}
1728+
}
1729+
} else if (convert.field() instanceof AbstractConvertFunction subConvert) {
1730+
return convertExpression.replaceChildren(
1731+
Collections.singletonList(resolveConvertFunction(subConvert, unionFieldAttributes))
1732+
);
1733+
}
16821734
return convertExpression;
16831735
}
16841736

@@ -1702,7 +1754,10 @@ private Expression createIfDoesNotAlreadyExist(
17021754
}
17031755
}
17041756

1705-
private MultiTypeEsField resolvedMultiTypeEsField(FieldAttribute fa, HashMap<TypeResolutionKey, Expression> typeResolutions) {
1757+
private static MultiTypeEsField resolvedMultiTypeEsField(
1758+
FieldAttribute fa,
1759+
HashMap<TypeResolutionKey, Expression> typeResolutions
1760+
) {
17061761
Map<String, Expression> typesToConversionExpressions = new HashMap<>();
17071762
InvalidMappedField imf = (InvalidMappedField) fa.field();
17081763
imf.getTypesToIndices().forEach((typeName, indexNames) -> {
@@ -1715,8 +1770,12 @@ private MultiTypeEsField resolvedMultiTypeEsField(FieldAttribute fa, HashMap<Typ
17151770
return MultiTypeEsField.resolveFrom(imf, typesToConversionExpressions);
17161771
}
17171772

1718-
private Expression typeSpecificConvert(ConvertFunction convert, Source source, DataType type, InvalidMappedField mtf) {
1773+
private static Expression typeSpecificConvert(ConvertFunction convert, Source source, DataType type, InvalidMappedField mtf) {
17191774
EsField field = new EsField(mtf.getName(), type, mtf.getProperties(), mtf.isAggregatable());
1775+
return typeSpecificConvert(convert, source, field);
1776+
}
1777+
1778+
private static Expression typeSpecificConvert(ConvertFunction convert, Source source, EsField field) {
17201779
FieldAttribute originalFieldAttr = (FieldAttribute) convert.field();
17211780
FieldAttribute resolvedAttr = new FieldAttribute(
17221781
source,
@@ -1971,4 +2030,32 @@ private static Aggregate transformAggregate(Aggregate aggregate) {
19712030
return aggregate.with(groupings, aggregatesWithNewRefs);
19722031
}
19732032
}
2033+
2034+
/**
2035+
* Cast the union typed fields in EsRelation to date_nanos if they are mixed date and date_nanos types.
2036+
*/
2037+
private static class ResolveUnionTypesInEsRelation extends Rule<LogicalPlan, LogicalPlan> {
2038+
@Override
2039+
public LogicalPlan apply(LogicalPlan plan) {
2040+
return plan.transformUp(EsRelation.class, relation -> {
2041+
if (relation.indexMode() == IndexMode.LOOKUP) {
2042+
return relation;
2043+
}
2044+
return relation.transformExpressionsUp(FieldAttribute.class, f -> {
2045+
if (f.field() instanceof InvalidMappedField imf && imf.types().stream().allMatch(DataType::isDate)) {
2046+
HashMap<ResolveUnionTypes.TypeResolutionKey, Expression> typeResolutions = new HashMap<>();
2047+
var convert = new ToDateNanos(f.source(), f);
2048+
imf.types().forEach(type -> {
2049+
ResolveUnionTypes.TypeResolutionKey key = new ResolveUnionTypes.TypeResolutionKey(f.name(), type);
2050+
var concreteConvert = ResolveUnionTypes.typeSpecificConvert(convert, f.source(), type, imf);
2051+
typeResolutions.put(key, concreteConvert);
2052+
});
2053+
var resolvedField = ResolveUnionTypes.resolvedMultiTypeEsField(f, typeResolutions);
2054+
return new FieldAttribute(f.source(), f.parentName(), f.name(), resolvedField, f.nullable(), f.id(), f.synthetic());
2055+
}
2056+
return f;
2057+
});
2058+
});
2059+
}
2060+
}
19742061
}

0 commit comments

Comments
 (0)