-
Notifications
You must be signed in to change notification settings - Fork 26k
Fix for union-types for multiple columns with the same name #110793
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
3511131
ead0948
5541673
a3b7933
5905a0f
0fb1c37
4ecf677
6f4833f
69fba3e
03a2255
5878131
3b4cea8
7d315f3
4aa2857
21352ad
99c093d
8839cad
26dff9b
2bde896
f6ba8ea
1b6a5a8
88e01b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -210,6 +210,20 @@ sample_data_str | 2023-10-23T12:27:28.948Z | 2764889 | Connected | |
| sample_data_str | 2023-10-23T12:15:03.360Z | 3450233 | Connected to 10.1.0.3 | ||
| ; | ||
|
|
||
| multiIndexSortIpString | ||
| required_capability: union_types | ||
| required_capability: casting_operator | ||
| required_capability: union_types_remove_fields | ||
|
|
||
| FROM sample_data, sample_data_str | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's worth adding here another more advanced variant (but still trying to keep the original query that brought up this issue):
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good one. |
||
| | SORT client_ip::ip | ||
| | LIMIT 1 | ||
| ; | ||
|
|
||
| @timestamp:date | client_ip:null | event_duration:long | message:keyword | ||
| 2023-10-23T13:33:34.937Z | null | 1232382 | Disconnected | ||
| ; | ||
|
|
||
| multiIndexIpStringStats | ||
| required_capability: union_types | ||
| required_capability: casting_operator | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,7 +133,12 @@ public enum Cap { | |
| * Fix a parsing issue where numbers below Long.MIN_VALUE threw an exception instead of parsing as doubles. | ||
| * see <a href="https://github.com/elastic/elasticsearch/issues/104323"> Parsing large numbers is inconsistent #104323 </a> | ||
| */ | ||
| FIX_PARSING_LARGE_NEGATIVE_NUMBERS; | ||
| FIX_PARSING_LARGE_NEGATIVE_NUMBERS, | ||
|
|
||
| /** | ||
| * Fix for union-types when sorting a type-casted field. We changed how we remove synthetic union-types fields. | ||
| */ | ||
| UNION_TYPES_REMOVE_FIELDS; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In related discussions we realized that adding capabilities to the end of the enum increases the risk of clashes when back-porting. Not a big issue, but requires a little extra manual step when back-porting.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved up a little, under the other union type capability. |
||
|
|
||
| private final boolean snapshotOnly; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,7 @@ | |
| import org.elasticsearch.xpack.esql.core.plan.TableIdentifier; | ||
| import org.elasticsearch.xpack.esql.core.rule.ParameterizedRule; | ||
| import org.elasticsearch.xpack.esql.core.rule.ParameterizedRuleExecutor; | ||
| import org.elasticsearch.xpack.esql.core.rule.Rule; | ||
| import org.elasticsearch.xpack.esql.core.rule.RuleExecutor; | ||
| import org.elasticsearch.xpack.esql.core.session.Configuration; | ||
| import org.elasticsearch.xpack.esql.core.tree.Source; | ||
|
|
@@ -60,6 +61,7 @@ | |
| import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.DateTimeArithmeticOperation; | ||
| import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.EsqlArithmeticOperation; | ||
| import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In; | ||
| import org.elasticsearch.xpack.esql.optimizer.rules.SubstituteSurrogates; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Aggregate; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Drop; | ||
| import org.elasticsearch.xpack.esql.plan.logical.Enrich; | ||
|
|
@@ -139,7 +141,13 @@ public class Analyzer extends ParameterizedRuleExecutor<LogicalPlan, AnalyzerCon | |
| new ResolveUnionTypes(), // Must be after ResolveRefs, so union types can be found | ||
| new ImplicitCasting() | ||
| ); | ||
| var finish = new Batch<>("Finish Analysis", Limiter.ONCE, new AddImplicitLimit(), new UnresolveUnionTypes()); | ||
| var finish = new Batch<>( | ||
| "Finish Analysis", | ||
| Limiter.ONCE, | ||
| new AddImplicitLimit(), | ||
| new UnresolveUnionTypes(), | ||
| new DropSyntheticUnionTypeAttributes() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels like these two rules could be only one, an union-types cleanup step kind of rule. They both do some very specific union-types operations and I don't see them not living together. I don't have an idea on how to merge them, but if you do and can do it, I kinda want to see them in only one rule.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll consolidate them. |
||
| ); | ||
| rules = List.of(init, resolution, finish); | ||
| } | ||
|
|
||
|
|
@@ -217,13 +225,13 @@ private static List<Attribute> mappingAsAttributes(Source source, Map<String, Es | |
| return list; | ||
| } | ||
|
|
||
| private static void mappingAsAttributes(List<Attribute> list, Source source, String parentName, Map<String, EsField> mapping) { | ||
| private static void mappingAsAttributes(List<Attribute> list, Source source, FieldAttribute parent, Map<String, EsField> mapping) { | ||
| for (Map.Entry<String, EsField> entry : mapping.entrySet()) { | ||
| String name = entry.getKey(); | ||
| EsField t = entry.getValue(); | ||
|
|
||
| if (t != null) { | ||
| name = parentName == null ? name : parentName + "." + name; | ||
| name = parent == null ? name : parent.fieldName() + "." + name; | ||
| var fieldProperties = t.getProperties(); | ||
| var type = t.getDataType().widenSmallNumeric(); | ||
| // due to a bug also copy the field since the Attribute hierarchy extracts the data type | ||
|
|
@@ -232,19 +240,16 @@ private static void mappingAsAttributes(List<Attribute> list, Source source, Str | |
| t = new EsField(t.getName(), type, t.getProperties(), t.isAggregatable(), t.isAlias()); | ||
| } | ||
|
|
||
| FieldAttribute attribute = t instanceof UnsupportedEsField uef | ||
| ? new UnsupportedAttribute(source, name, uef) | ||
| : new FieldAttribute(source, parent, name, t); | ||
| // primitive branch | ||
| if (EsqlDataTypes.isPrimitive(type)) { | ||
| Attribute attribute; | ||
| if (t instanceof UnsupportedEsField uef) { | ||
| attribute = new UnsupportedAttribute(source, name, uef); | ||
| } else { | ||
| attribute = new FieldAttribute(source, null, name, t); | ||
| } | ||
| list.add(attribute); | ||
| } | ||
| // allow compound object even if they are unknown (but not NESTED) | ||
| if (type != NESTED && fieldProperties.isEmpty() == false) { | ||
| mappingAsAttributes(list, source, name, fieldProperties); | ||
| mappingAsAttributes(list, source, attribute, fieldProperties); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -1104,23 +1109,21 @@ protected LogicalPlan doRule(LogicalPlan plan) { | |
| plan = plan.transformExpressionsOnly(UnresolvedAttribute.class, ua -> resolveAttribute(ua, resolved)); | ||
| } | ||
|
|
||
| // Otherwise drop the converted attributes after the alias function, as they are only needed for this function, and | ||
| // the original version of the attribute should still be seen as unconverted. | ||
| plan = dropConvertedAttributes(plan, unionFieldAttributes); | ||
|
|
||
| // And add generated fields to EsRelation, so these new attributes will appear in the OutputExec of the Fragment | ||
| // and thereby get used in FieldExtractExec | ||
| plan = plan.transformDown(EsRelation.class, esr -> { | ||
| List<Attribute> output = esr.output(); | ||
| List<Attribute> missing = new ArrayList<>(); | ||
| for (FieldAttribute fa : unionFieldAttributes) { | ||
| if (output.stream().noneMatch(a -> a.id().equals(fa.id()))) { | ||
| // Using outputSet().contains looks by NameId, resp. uses semanticEquals. | ||
| if (esr.outputSet().contains(fa) == false) { | ||
| missing.add(fa); | ||
| } | ||
| } | ||
|
|
||
| if (missing.isEmpty() == false) { | ||
| output.addAll(missing); | ||
| return new EsRelation(esr.source(), esr.index(), output, esr.indexMode(), esr.frozen()); | ||
| List<Attribute> newOutput = new ArrayList<>(esr.output()); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I definitely missed the subtlety of the mutating state behaviour here, and did not realize this was why my earlier edit to EsRelation could be deleted. Thanks for fixing this and bringing back EsRelation fixes. |
||
| newOutput.addAll(missing); | ||
| return new EsRelation(esr.source(), esr.index(), newOutput, esr.indexMode(), esr.frozen()); | ||
| } | ||
| return esr; | ||
| }); | ||
|
|
@@ -1136,17 +1139,6 @@ private Expression resolveAttribute(UnresolvedAttribute ua, Map<Attribute, Expre | |
| }; | ||
| } | ||
|
|
||
| private LogicalPlan dropConvertedAttributes(LogicalPlan plan, List<FieldAttribute> unionFieldAttributes) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice that this could finally go. It did look wrong. |
||
| List<NamedExpression> projections = new ArrayList<>(plan.output()); | ||
| for (var e : unionFieldAttributes) { | ||
| projections.removeIf(p -> p.id().equals(e.id())); | ||
| } | ||
| if (projections.size() != plan.output().size()) { | ||
| return new EsqlProject(plan.source(), plan, projections); | ||
| } | ||
| return plan; | ||
| } | ||
|
|
||
| private Expression resolveConvertFunction(AbstractConvertFunction convert, List<FieldAttribute> unionFieldAttributes) { | ||
| if (convert.field() instanceof FieldAttribute fa && fa.field() instanceof InvalidMappedField imf) { | ||
| HashMap<TypeResolutionKey, Expression> typeResolutions = new HashMap<>(); | ||
|
|
@@ -1175,7 +1167,13 @@ private Expression createIfDoesNotAlreadyExist( | |
| MultiTypeEsField resolvedField, | ||
| List<FieldAttribute> unionFieldAttributes | ||
| ) { | ||
| var unionFieldAttribute = new FieldAttribute(fa.source(), fa.name(), resolvedField); // Generates new ID for the field | ||
| // Generate new ID for the field and suffix it with the data type to maintain unique attribute names. | ||
| String unionTypedFieldName = SubstituteSurrogates.rawTemporaryName( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The more I see
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree and so I just tried that; it would make sense to move to Maybe let's leave this for another time to not cause too much code movement; this PR needs backporting to 8.15, that could become harder if we also refactor outside of the PR's immediate scope. |
||
| fa.name(), | ||
| "converted_to", | ||
| resolvedField.getDataType().typeName() | ||
| ); | ||
| FieldAttribute unionFieldAttribute = new FieldAttribute(fa.source(), fa.parent(), unionTypedFieldName, resolvedField); | ||
| int existingIndex = unionFieldAttributes.indexOf(unionFieldAttribute); | ||
| if (existingIndex >= 0) { | ||
| // Do not generate multiple name/type combinations with different IDs | ||
|
|
@@ -1235,4 +1233,30 @@ static Attribute checkUnresolved(FieldAttribute fa) { | |
| return fa; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * {@link ResolveUnionTypes} creates new, synthetic attributes for union types: | ||
| * If {@code client_ip} is present in 2 indices, once with type {@code ip} and once with type {@code keyword}, | ||
| * using {@code EVAL x = to_ip(client_ip)} will create a single attribute @{code $$client_ip$converted_to$ip}. | ||
| * This should not spill into the query output, so we drop such attributes at the end. | ||
| */ | ||
| private static class DropSyntheticUnionTypeAttributes extends Rule<LogicalPlan, LogicalPlan> { | ||
| public LogicalPlan apply(LogicalPlan plan) { | ||
| List<Attribute> output = plan.output(); | ||
| List<Attribute> newOutput = new ArrayList<>(output.size()); | ||
|
|
||
| for (Attribute attr : output) { | ||
| // TODO: this should really use .synthetic() | ||
| // https://github.com/elastic/elasticsearch/issues/105821 | ||
| if (attr.name().startsWith(FieldAttribute.SYNTHETIC_ATTRIBUTE_NAME_PREFIX) == false) { | ||
| newOutput.add(attr); | ||
| } | ||
| } | ||
|
|
||
| if (newOutput.size() != output.size()) { | ||
| return new Project(Source.EMPTY, plan, newOutput); | ||
| } | ||
| return plan; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,6 +104,13 @@ public UnsupportedEsField field() { | |
| return (UnsupportedEsField) super.field(); | ||
| } | ||
|
|
||
| @Override | ||
| public String fieldName() { | ||
| // The super fieldName uses parents to compute the path; this class ignores parents, so we need to rely on the name instead. | ||
| // Using field().getName() would be wrong: for subfields like parent.subfield that would return only the last part, subfield. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice observation! |
||
| return name(); | ||
| } | ||
|
|
||
| @Override | ||
| protected NodeInfo<FieldAttribute> info() { | ||
| return NodeInfo.create(this, UnsupportedAttribute::new, name(), field(), hasCustomMessage ? message : null, id()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -140,7 +140,8 @@ else if (plan instanceof Project project) { | |
| Map<DataType, Alias> nullLiteral = Maps.newLinkedHashMapWithExpectedSize(DataType.types().size()); | ||
|
|
||
| for (NamedExpression projection : projections) { | ||
| if (projection instanceof FieldAttribute f && stats.exists(f.qualifiedName()) == false) { | ||
| // Do not use the attribute name, this can deviate from the field name for union types. | ||
| if (projection instanceof FieldAttribute f && stats.exists(f.fieldName()) == false) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps in the fieldName javadoc we should mention why we don't use qualified name (the fact that qualifiers are always null in ESQL, and this is an artifact of SQL code)?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hope we can merge #110581 soon, making the comment here very short lived. Update I mean any comment about qualified names, not the existing comment. In any case, the qualified name might even be pretty wrong here - our indices don't know about qualifiers, only field names! |
||
| DataType dt = f.dataType(); | ||
| Alias nullAlias = nullLiteral.get(f.dataType()); | ||
| // save the first field as null (per datatype) | ||
|
|
@@ -170,7 +171,8 @@ else if (plan instanceof Project project) { | |
| || plan instanceof TopN) { | ||
| plan = plan.transformExpressionsOnlyUp( | ||
| FieldAttribute.class, | ||
| f -> stats.exists(f.qualifiedName()) ? f : Literal.of(f, null) | ||
| // Do not use the attribute name, this can deviate from the field name for union types. | ||
| f -> stats.exists(f.fieldName()) ? f : Literal.of(f, null) | ||
| ); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,7 +98,7 @@ public boolean expressionsResolved() { | |
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hash(index, indexMode, frozen); | ||
| return Objects.hash(index, indexMode, frozen, attrs); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -112,7 +112,10 @@ public boolean equals(Object obj) { | |
| } | ||
|
|
||
| EsRelation other = (EsRelation) obj; | ||
| return Objects.equals(index, other.index) && indexMode == other.indexMode() && frozen == other.frozen; | ||
| return Objects.equals(index, other.index) | ||
| && indexMode == other.indexMode() | ||
| && frozen == other.frozen | ||
| && Objects.equals(attrs, other.attrs); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excellent. I had this fix in an earlier version of union-types, but removed it when I discovered it was not needed. I did not realize that it was only not needed due to another bug. It feels good to see this code come back.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's quite important; this can mask test failures (it did), and it can also prevent optimizations from taking effect if all they do is change an
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is needed because now (sometime last year when EsRelation has been changed to work "better" in ESQL) |
||
| } | ||
|
|
||
| @Override | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -117,7 +117,8 @@ public final PhysicalOperation fieldExtractPhysicalOperation(FieldExtractExec fi | |
| DataType dataType = attr.dataType(); | ||
| MappedFieldType.FieldExtractPreference fieldExtractPreference = PlannerUtils.extractPreference(docValuesAttrs.contains(attr)); | ||
| ElementType elementType = PlannerUtils.toElementType(dataType, fieldExtractPreference); | ||
| String fieldName = attr.name(); | ||
| // Do not use the field attribute name, this can deviate from the field name for union types. | ||
| String fieldName = attr instanceof FieldAttribute fa ? fa.fieldName() : attr.name(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was wondering, surely all attributes in this method must be field attributes?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think metadata attributes, like |
||
| boolean isUnsupported = dataType == DataType.UNSUPPORTED; | ||
| IntFunction<BlockLoader> loader = s -> getBlockLoaderFor(s, fieldName, isUnsupported, fieldExtractPreference, unionTypes); | ||
| fields.add(new ValuesSourceReaderOperator.FieldInfo(fieldName, elementType, loader)); | ||
|
|
@@ -235,8 +236,10 @@ public final Operator.OperatorFactory ordinalGroupingOperatorFactory( | |
| // Costin: why are they ready and not already exposed in the layout? | ||
| boolean isUnsupported = attrSource.dataType() == DataType.UNSUPPORTED; | ||
| var unionTypes = findUnionTypes(attrSource); | ||
| // Do not use the field attribute name, this can deviate from the field name for union types. | ||
| String fieldName = attrSource instanceof FieldAttribute fa ? fa.fieldName() : attrSource.name(); | ||
| return new OrdinalsGroupingOperator.OrdinalsGroupingOperatorFactory( | ||
| shardIdx -> getBlockLoaderFor(shardIdx, attrSource.name(), isUnsupported, NONE, unionTypes), | ||
| shardIdx -> getBlockLoaderFor(shardIdx, fieldName, isUnsupported, NONE, unionTypes), | ||
| vsShardContexts, | ||
| groupElementType, | ||
| docChannel, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this var here improves readbility. It's used only once and can be removed.