Skip to content

ESQL: Union Types Support#107545

Merged
craigtaverner merged 12 commits intoelastic:mainfrom
craigtaverner:union_types_take3
Jun 19, 2024
Merged

ESQL: Union Types Support#107545
craigtaverner merged 12 commits intoelastic:mainfrom
craigtaverner:union_types_take3

Conversation

@craigtaverner
Copy link
Copy Markdown
Contributor

@craigtaverner craigtaverner commented Apr 16, 2024

If the query sources multiple indexes, and the same field exists in multiple indexes with different types, this would normally fail the query. However, if the query includes a conversion function to resolve the field to a single type before it is used in other functions or aggregations, then this should work.

The following query works in this third prototype:

FROM sample_data* METADATA _index
| EVAL client_ip = TO_IP(client_ip)
| KEEP _index, @timestamp, client_ip, event_duration, message
| SORT _index ASC, @timestamp DESC

The client_ip field is an IP in the sample_data index, but a keyword in the sample_data_str index.

The first prototype did stuff to the drivers to create an index specific DriverContext to use during field evaluator construction so that the conversion function would be index/type aware. However, that abuses the idea of multi-threaded drivers. So the second prototype took a new approach to instead re-plan the logical plan to extract the converter from the EVAL expressions, setting them as resolved (claiming the input type is already the converted type), and stored the converter in the EsRelation for later use in Physical planning. This third prototype takes this further by replacing the conversion function with a new FieldAttribute. ANd both old and new FieldAttributes exist in parallel, so that the logic around handling unsupported fields is not changed.

Fixes #100603

Tasks to do:

  • Develop prototype that plans type conversion from the eval to the field extraction
  • Cleanup classes and types
    • Move MultiTypeField to esql/types
    • Add serialization of MultiTypeField to PlanNamedTypes for multi-node cluster support
  • Generate correct error messages when plan cannot be resolved (mimic previous behaviour)
  • Change solution to always create a new field (same name, different id) to not clash with existing field semantics
  • Support rowStrideReader (using a conversion function directly in the ValueSourceReaderOperator)
    • in LoadFromMany
    • in loadFromSingleLeaf
  • Support conversion expression in any part of the plan (not just EVAL)
  • More tests cases
    • get union_types.csv-spec to work in CsvTests (or disable like ENRICH)
    • Test with conversion function in WHERE clause
    • test more complex cases (multiple evals, convert types back and forth, with and without stats)
    • Unit tests for ValueSourceReaderOperator enhancements
    • tests covering row-stride-reader (synthetic source?)
  • Bugs
    • Fields with type Date are not working
    • Using invalid convert functions leads to incorrect error message (get runtime error instead of validation error)
    • With no KEEP we get multiple columns with the same name
  • Union types documentation #110183

@craigtaverner craigtaverner added >enhancement Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) :Analytics/ES|QL AKA ESQL labels Apr 16, 2024
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

Hi @craigtaverner, I've created a changelog YAML for you.

@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

Hi @craigtaverner, I've created a changelog YAML for you.

Copy link
Copy Markdown
Member

@costin costin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a round of comments.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule tries to rollback something that either shouldn't have occurred in the first place or that is ignored by the verifier.
If there's no AbstractConvertFunction, MultiTypeFields can considered unsupported in projections and unsupported inside expressions (see EsqlProject and the Verifier).

Copy link
Copy Markdown
Contributor Author

@craigtaverner craigtaverner Apr 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule does something that used to be done by the ResolveRefs code. We have a catch-22 situation where ResolveUnionTypes depends on ResolveRefs to run first, but also relies on InvalidMappedField (or currently MultiTypeEsField.UnresolvedField) to not be converted into an UnresolvedAttribute (which ResolveRefs also does). So that conversion needed to be removed from ResolveRefs and moved after ResolveUnionTypes. This is that new location. This code also did something much more complex before, also editing the contents of EsRelation and the contained EsIndex, but that was getting very messy, and only needed to handle regression tests around unsupported types. I had three choices:

  • Continue to make UnresolveUnionTypes more complex (I got all but one regression test passing, so just needed to add nested resolution of Object fields)
  • Find a way to get MultiTypeEsField.UnresolvedField to pass through Plan serialization (this is the route I took, but it looks hacky, and I see your comment on that already). This allowed me to reduce this class a lot (you see the simplified version here).
  • Merge MultiTypeEsField.UnresolvedField with InvalidMappedField so we avoid the plan serialisation hack. This is my new preferred approach, and I see you suggest this approach too in another comment.

But in none of these cases does this class completely disappear. Somewhere we need to recognise InvalidMappedField as an unresolved type. That used to happen in ResolveRefs, and now happens here. You are suggesting an alternative approach/location? Verifying EsqlProject? I can investigate that on Monday. I've looked through the Verifier a few times, so I can imagine possibilities there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - it's a minor point but you could incorporate the rule as part of the UnionType rules in a second pass.
That is convert all InvalidMappedFields that were are not wrapped in a conversion function to the unresolved attribute.
Again minor.

Comment on lines 315 to 320
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be causing more problems than it tries to fix. Allow AbstractConvertFunction to work on InvalidMappedField and replace them with MultiType/ConvertedEsField.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. It did actually fix a bunch of things, because UnresolveUnionTypes was becoming quite complex in order to get regressions to pass, and putting this here made it much simpler. However, if I instead combine the classes InvalidMappedField and MultiTypeEsField.UnresolvedField into one class, that same simplification occurs, without this hack. I noticed a lot of failures in CI that I could not reproduce locally, but this particular hack feels like a likely source of the failures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, in case it was not clear, the existence of this in the plan was only when there is no AbstractConvertFunction, so the remaining InvalidMappedField needs to be serialised in the plan, to support existing behaviour around unsupported types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use InvalidMappedField directly since that is already picked up by the analyzer/verifier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since InvalidMappedField was in QL, I did not want to touch it, but I'm been coming round more and more to the idea that I really should. I think it will make things much simpler, and avoid the awkward PlanNamedTypes hack.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conversation has come full circle now, due to the recent reviews asking to not edit QL directly. After the above comment, I switched to editing QL directly. There were two classes where we edited QL FieldAttribute where the equals and hashcode were updated to include the underlying field, so query plan re-writing would work (could be seen as an oversight in QL code, so perhaps even a bug-fix), and the above mentioned InvalidMappedField. An attempt to completely extricate these two from QL failed (dependencies increased scope dramatically, turning a moderate PR into a monster). A second attempt at a compromise by making ESQL versions that maintained the simple name, and extended the QL versions worked for both classes, but was quite messy for FieldAttribute. After discussing with the team, we decided to keep the ESQL port of InvalidMappedField, but revert to the direct edit of the FieldAttribute. The thinking here is the up-coming split of QL and ESQL would be slightly facilitated by not having any edits in InvalidMappedField, while the edit to FieldAttribute we would probably want to keep anyway.

Copy link
Copy Markdown
Member

@costin costin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a round of comments.

@craigtaverner craigtaverner force-pushed the union_types_take3 branch 3 times, most recently from 4125129 to cc66455 Compare April 24, 2024 16:36
@craigtaverner craigtaverner changed the title ESQL: Union Types Support (take3) ESQL: Union Types Support May 2, 2024
@craigtaverner craigtaverner marked this pull request as ready for review May 2, 2024 17:17
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-analytical-engine (Team:Analytics)

@craigtaverner
Copy link
Copy Markdown
Contributor Author

After some recent refinements, and the addition of the unit tests in ValuesSourceReaderTypeConversionTests, I think it is time for some reviews!

Copy link
Copy Markdown
Member

@luigidellaquila luigidellaquila left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Craig, I did a couple of tests, but I think I need some guidance to understand which types/functions this PR covers, because I couldn't manage to make it work...

I did the following:

  • created two indexes, test1 and test2 with only one @timestamp field of type long and date respectively.
  • tried a few queries with empty indexes:
    • from test* | eval x = to_long(@timestamp) | keep x: all good
    • from test* | eval x = to_string(@timestamp) | keep x: all good
    • from test* | eval x = to_date(@timestamp) | keep x: verification_exception, Found 1 problem\nline 1:31: Cannot use field [@timestamp] due to ambiguities being mapped as [2] incompatible types: [date] in [test2], [long] in [test1]

Then I added one record per index

{"index": {"_index":"test1"}}
{"@timestamp":10000000}
{"index": {"_index":"test2"}}
{"@timestamp":"2022-05-06T12:01:00.000Z"}

and tried the same queries again:

  • from test* | eval x = to_long(@timestamp) | keep x: null_pointer_exception, Cannot invoke \"Object.hashCode()\" because \"pk\" is null
  • from test* | eval x = to_string(@timestamp) | keep x: same as above
  • from test* | eval x = to_date(@timestamp) | keep x: verification_exception. Found 1 problem\nline 1:31: Cannot use field [@timestamp] due to ambiguities being mapped as [2] incompatible types: [date] in [test2], [long] in [test1]
  • from test* | eval x = to_ip(@timestamp) | keep x: esql_illegal_argument_exception, illegal data type [long] (suppressed null_pointer_exception as above)

I also tried IP vs long and I got some results with to_string() but not with other functions.

So a couple of questions:

  • does this cover all the types? I didn't find any type-specific code in the PR, so I assumed it was the case.
  • is it supposed to handle partially invalid conversions? Eg. if I have an IP and a date and I'm only interested in the date, can I do it?

[edit] it should be to_datetime() (to_date() does not exist); probably the validation order makes it trip on the incompatible types before it realizes that the function name is wrong, it makes the message a bit confusing, but probably it's a minor problem.
The above queries, with to_datetime() return the same error as to_long()

@craigtaverner
Copy link
Copy Markdown
Contributor Author

The issues with @timestamp have been fixed in #6fb0622dc43070aa3c71c425d405ac273bf43d45, and more tests added in that and other commits to cover this case. The related issue regarding exactly which error message to return can be dealt with later, perhaps in this PR, or perhaps in another.

@nik9000
Copy link
Copy Markdown
Member

nik9000 commented May 3, 2024

I feel bad for the github user @timestamp. We ping them so much.

Copy link
Copy Markdown
Member

@nik9000 nik9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few comments. I suggested reworking how row-by-row loading is modeled with the BlockBuilders inside of the value loaders themselves. This feels like it's much cleaner to think about. I think we can make it compatible with a shared loader if we need to. But for now the copying seems fine because this is never the hottest path.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the BlockBuilder and converter inside the block loader itself? That'd rework the row-stride block loader somewhat, but I think that'd make this more readable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember originally trying the combine these, but seem to remember it causing hassles with the ColumnAtATimeReader. This was before I really got the row-stride reader working, so perhaps it is time for another attempt.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be ok delaying it to a followup too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realise a key difference between what you suggested and what I tried (and was thinking of trying again). You said to put the builder and converter inside the loader. Instead I had tried to put the converter inside the builder. This is because we will always have both a builder and loader (the builder is made by the loader once additional information is known using code like field.loader.builder(loaderBlockFactory, docs.count())), so merging the loader and builder seems unworkable. Right now the converter is the only thing at play. It is currently implemented by the loader, since it does not need any of the information in the builder. But this does lead to lines like this:

Block build() {
    return (Block) loader.convert(builder.build());
}

If I put the converter into the builder, we can hide it inside the build() method call, simplifying things slightly. That was what I was trying before, and could try again.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to replace the IT_tests_only suffix with a check against a feature similar to how we do version testing. We're already going to add features for BWC skipping, we can add a skip in the hand-rolled CSV testing infrastructure too. I did it for one example here: #108313

Would you be ok doing the same on this one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I've done this now, will push soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you include the evaluator too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying this one scares me a bit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of modifying this, we should create a copy in the esql project.

This may involve some yak shaving, but I'm happy to help with that!

Copy link
Copy Markdown
Member

@costin costin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have minor comments around the logistics and code dependency - such as moving the QL classes like FieldAttribute, EsRelation and InvalidMapperField to ESQL.
An alternative would be to have some workaround code in ESQL that adds/maintains that information outside of QL (even though it's ugly) and then get rid of that during the QL migration to not postpone this PR any longer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR - we need to come up with a naming strategy for the features so we can reason about when they were added without having to open this file which is already too long.
I propose YY_MM.esql.feature_name pattern with the date prefix used in the variable name as well:

NodeFeature 24_05_UNION_TYPES = new NodeFeature("24_05.esql.union_types")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like a solution to a problem we suspect we might get in future, but do not (yet) suffer from, the issue of the creation of too many NodeFeatures. I think there are many solutions to that problem, like regular removal of older NodeFeature instances and references to them, since they should only really matter for BWC and rollover of recent releases. Or some other solution... let's not make it part of this PR (or any feature PR).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm with @craigtaverner on this. I'd prefer not to adopt a new pattern here. I'm not really sure what the pattern should be either. Or if we need one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And add the features here in reverse order, so the latest features are at the top (that's because in time, the early features become wildly spread).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's pretty reasonable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just stick this one on top and leave a comment saying add new ones on top.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for those reading - it's an unordered Set - but we humans do read it in order.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far it seems everyone, including me, has be appending to the end. So we're talking about reversing the order of the complete list? I can do that, although I don't fully understand the reason for this. The term 'wildly spread' does not clarify things for me.

Comment on lines 42 to 73
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: move these methods at the bottom since their non-essential.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clone this to ESQL and add the modifications there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I'll discuss this with @alex-spies and do it together with the FieldAttribute changes, since both are really part of the 'split from QL' project.

Copy link
Copy Markdown
Contributor Author

@craigtaverner craigtaverner May 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for InvalidMappedField, but we decided to keep the QL changes in FieldAttribute. Moving that was a much bigger job, and it was argued that these changes are an improvement anyway.

@costin costin requested a review from astefan May 7, 2024 02:04
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EsRelation was already ported to esql by Nhat - I think this fix should be applied to esql's copy of that, not here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curiously, in the QL version, it used attrs in the hashCode, but not in the equals method. My change adds it to equals. Nhat's version removed it from hashCode. I'm curious if his change fixed something else, that I will now break.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out the changes I made were no longer necessary, and only needed for an earlier version of union-types. I kept just one clarifying comment.

Copy link
Copy Markdown
Member

@costin costin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's room for improvement however this PR took long enough and github doesn't like this commit history.
I'd wait for Andrei's comments, create a separate issue with feedback, merge this in and address the remaining issues in a separate PR.

Thanks for your patience Craig.

@craigtaverner craigtaverner removed request for a team June 12, 2024 08:25
Note that one test, `multiIndexIpStringStatsInline` is muted due to failing with the error:

    UnresolvedException: Invalid call to dataType on an unresolved object ?client_ip
Copy link
Copy Markdown
Contributor

@astefan astefan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding tests. I like them.
I've left few very minor comments and one that is more serious.
I think there is value in the PR and it should be merged, but it needs follow-ups.

LGTM.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover or follow up item? If it's the latter it would help having a gh issue created.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here about a followup issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method and the next 5-6 are borrowed from OperatorTestCase. Can you reuse them instead? (this test class is already big and it would help to have a more manageable size)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this well, you remove these attributes assuming they are only used in the "alias function" by which you mean an eval x = field::type. I don't think eval is the only place where you can place such a conversion function.

STATS c=count(*) BY client_ip::ip

FROM sample_data, sample_data_str | SORT client_ip::ip

Both of these have issues. After you merge this PR, I will create follow-ups for these two.

Copy link
Copy Markdown
Member

@luigidellaquila luigidellaquila left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

The integration tests do not fail the tests if the capability does not even exist on cluster nodes, instead the tests are ignored. The same behaviour should happen with CsvTests for consistency.
This way we don't have to add more features to the test framework in this PR, but we would probably want a mute feature (like a `skip` line).
Since the sub-fields are AbstractConvertFunction expressions, and Expression is not yet fully supported as a category class for NamedWritable, we need a few slight tweaks to this, notably registering this explicitly in the EsqlPlugin, as well as calling PlanStreamInput.readExpression() instead of StreamInput.readNamedWritable(Expression.class). These can be removed later once Expression is fully supported as a category class.
We used required_capability to mute the tests, but this caused issues with CsvTests which also uses this as a spelling mistake checker for typing the capability name wrong, so we tried to use muted-tests.yml, but that only mutes tests in specific run configurations (ie. we need to mute each and every IT class separately).

So now we just remove the tests entirely. We left a comment in the muted-tests.yml file for future reference about how to mute csv-spec tests.
in.readString(),
DataType.readFrom(in),
in.readBoolean(),
in.readImmutableMap(StreamInput::readString, i -> ((PlanStreamInput) i).readExpression())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to just do readMap( i -> ((PlanStreamInput) i).readExpression())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a little more convenient and I just trust that we don't mutate the map.

Though I should probably do:

readImutableMap(i -> ((PlanStreamInput) i).readExpression())

Copy link
Copy Markdown
Member

@nik9000 nik9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@craigtaverner told me that the concurrent serialization tests are taking a surprising amount of time on this PR so I'm going to check that.

out.writeString(getName());
out.writeString(getDataType().typeName());
out.writeBoolean(isAggregatable());
out.writeMap(getIndexToConversionExpressions(), (o, v) -> out.writeNamedWriteable(v));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
This what we need. It lines up perfectly with writeNamed from the old stuff.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I don't believe this is going to work yet. Expression isn't yet fully properly implemented as a NamedWriteable. You'd be better off doing

out.writeMap(getIndexToConversionExpressions(), (o, v) -> ((PlanStreamOutput) o).writeExpression(v))

I'm in the process of converting all remaining Expression subclasses to proper NamedWriteables. they don't all properly implement it yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yet it does work. At least my multi-node integration tests pass, and they rely on this serialisation succeeding (both for write and read). I assumed it was because the on-the-wire serialization was the same in both cases.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll only work if we've implemented NamedWriteable for all the subclasses you want to serialize. I'm doing that with stuff like #109892 but not all subclasses are done. I suppose if you only allow certain stuff in there it'll be fine. I dunno.

in.readString(),
DataType.readFrom(in),
in.readBoolean(),
in.readImmutableMap(StreamInput::readString, i -> ((PlanStreamInput) i).readExpression())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a little more convenient and I just trust that we don't mutate the map.

Though I should probably do:

readImutableMap(i -> ((PlanStreamInput) i).readExpression())

}

public MultiTypeEsField(StreamInput in) throws IOException {
// TODO: Change the conversion expression serialization to i.readNamedWriteable(Expression.class) once Expression is fully supported
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That TODO will get done before too long. I'll remove readExpression entirely.

@@ -0,0 +1,203 @@
setup:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word "nested" means the nested type to me. Could you rename this file to something like "sub_fields" or something? I forgot what we call those sub field things, but nested and object mean something specific to me.

@craigtaverner
Copy link
Copy Markdown
Contributor Author

@elasticmachine update branch

Copy link
Copy Markdown
Contributor

@bpintea bpintea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving some comments that could be considered for later.

return new Exact(false, "Field [" + getName() + "] is invalid, cannot access it");
}

public Map<String, Set<String>> getTypesToIndices() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: just to preserve the getters style.

Suggested change
public Map<String, Set<String>> getTypesToIndices() {
public Map<String, Set<String>> typesToIndices() {

}

/**
* Constructor supporting union types, used in ES|QL.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: since it's no longer in QL.

Suggested change
* Constructor supporting union types, used in ES|QL.
* Constructor supporting union types.

/**
* Representation of field mapped differently across indices.
* Used during mapping discovery only.
* Note that the field <code>typesToIndices</code> is not serialized because that information is
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if there's an actual valid case when such an object does get serialised (outside tests, that is).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There really should not be. This class, InvalidMappedField is used in the Analyzer to produce either UnsupportedField, or MultiTypeEsField (the non-union-types and the union-types), long before any serialization. The typesToIndices is used to produce the think that those two fields need, either and error message, or the union-types converters (which are serialized as part of MultiTypeEsField).

What would make sense in future, once the port to NamedWritable is complete, would be for this class to throw an exception!

return plan;
}

private Expression resolveConvertFunction(AbstractConvertFunction convert, List<FieldAttribute> unionFieldAttributes) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the private methods in this class can be made static.

return unsupported(name, fc);
}
typesToIndices.computeIfAbsent(type.esType(), _key -> new TreeSet<>()).add(ir.getIndexName());
typesToIndices.computeIfAbsent(type.typeName(), _key -> new TreeSet<>()).add(ir.getIndexName());
Copy link
Copy Markdown
Contributor

@bpintea bpintea Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use the type as key? That'll make the repeated type resolution in ResolveUnionTypes#resolveConvertFunction() redundant.
Edit: and #resolvedMultiTypeEsField().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think I just minimized the differences to the original code, but your idea makes sense.

TypeResolutionKey key = new TypeResolutionKey(fa.name(), type);
var concreteConvert = typeSpecificConvert(convert, fa.source(), type, imf);
typeResolutions.put(key, concreteConvert);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the the alternative branch to this if return convert; already? And the check below skipped? If in one index the field is mapped as a type that the conversion function doesn't support, there shouldn't be any MultiTypeEsField created anyways (and the analysis would converge sooner).

return MultiTypeEsField.resolveFrom(imf, typesToConversionExpressions);
}

private Expression typeSpecificConvert(AbstractConvertFunction convert, Source source, DataType type, InvalidMappedField mtf) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private Expression typeSpecificConvert(AbstractConvertFunction convert, Source source, DataType type, InvalidMappedField mtf) {
private Expression typeSpecificConvert(AbstractConvertFunction convert, Source source, DataType type, InvalidMappedField imf) {

@craigtaverner craigtaverner merged commit d1e3c0a into elastic:main Jun 19, 2024
Copy link
Copy Markdown
Contributor

@bpintea bpintea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And a last batch of (post-merge) notes to potentially be later considered.
Very nice.

imf.getTypesToIndices().forEach((typeName, indexNames) -> {
DataType type = DataType.fromTypeName(typeName);
TypeResolutionKey key = new TypeResolutionKey(fa.name(), type);
if (typeResolutions.containsKey(key)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can there be a case this check is false?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if there are not any convert functions for the types found in the index mappings. Perhaps we have more than one convert function needed, but only one exists in the query. I'm pretty sure I have integration tests (yml tests) that cover this case.

}

private MultiTypeEsField resolvedMultiTypeEsField(FieldAttribute fa, HashMap<TypeResolutionKey, Expression> typeResolutions) {
Map<String, Expression> typesToConversionExpressions = new HashMap<>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Map<String, Expression> typesToConversionExpressions = new HashMap<>();
Map<String, Expression> typesToConversionExpressions = new HashMap<>(typeResolutions.size());


/**
* During IndexResolution it could occur that the same field is mapped to different types in different indices.
* The class MultiTypeEfField.UnresolvedField holds that information and allows for later resolution of the field
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnresolvedField doesn't exist (anymore?).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, this comment reflects an earlier version of the class. That is now merged into InvalidMappedField, and not its own class, since we moved QL to ESQL.CORE:

return ENTRY.name;
}

public Map<String, Expression> getIndexToConversionExpressions() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: same style comment as in previous batch about the getter, which we usually have w/o the get prefix when it matches the field name. But not sure if it's that widespread.

Map<String, Expression> typesToConversionExpressions
) {
Map<String, Set<String>> typesToIndices = invalidMappedField.getTypesToIndices();
DataType resolvedDataType = DataType.UNSUPPORTED;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
DataType resolvedDataType = DataType.UNSUPPORTED;
DataType resolvedDataType = null;

I guess init'ing to DataType.UNSUPPORTED is safe and maybe caught later down the path (if caused by defect), but it should be an error if resolvedDataType isn't updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/ES|QL AKA ESQL >enhancement Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v8.15.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ESQL: Mismatching mapping cannot be worked around with eval (Support for union types)

10 participants