-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Disallow CCS with lookup join #120277
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
Disallow CCS with lookup join #120277
Changes from 5 commits
1b99a4d
b8cf1b3
b1a0280
5f55608
57a0df6
c9b077e
c6be044
6a94eaf
d283968
66dd33c
343d104
416a8fc
a6aa610
59b78b7
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 |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| import org.elasticsearch.compute.data.Block; | ||
| import org.elasticsearch.index.IndexMode; | ||
| import org.elasticsearch.logging.Logger; | ||
| import org.elasticsearch.transport.RemoteClusterAware; | ||
| import org.elasticsearch.xpack.core.enrich.EnrichPolicy; | ||
| import org.elasticsearch.xpack.esql.Column; | ||
| import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; | ||
|
|
@@ -653,6 +654,28 @@ private Join resolveLookupJoin(LookupJoin join) { | |
| return join; | ||
| } | ||
|
|
||
| // joining with remote cluster is not supported yet | ||
| if (join.left() instanceof EsRelation esr) { | ||
| for (var index : esr.index().concreteIndices()) { | ||
| if (index.indexOf(RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR) != -1) { | ||
|
||
| return join.withConfig( | ||
| new JoinConfig( | ||
| type, | ||
| List.of( | ||
| new UnresolvedAttribute( | ||
| join.source(), | ||
| "unsupported", | ||
| "LOOKUP JOIN does not support joining with remote cluster indices [" + esr.index().name() + "]" | ||
| ) | ||
| ), | ||
| List.of(), | ||
| List.of() | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| JoinType coreJoin = using.coreJoin(); | ||
| // verify the join type | ||
| if (coreJoin != JoinTypes.LEFT) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2250,6 +2250,24 @@ public void testLookupJoinIndexMode() { | |
| assertThat(e.getMessage(), containsString("1:25: invalid [test] resolution in lookup mode to an index in [standard] mode")); | ||
| } | ||
|
|
||
| public void testLookupJoinRemoteClusterUnsupported() { | ||
| assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V11.isEnabled()); | ||
|
|
||
| var remoteIndexResolution = loadMapping("mapping-default.json", "remote:my-index"); | ||
| var lookupResolution = AnalyzerTestUtils.defaultLookupResolution(); | ||
| VerificationException e = expectThrows( | ||
| VerificationException.class, | ||
| () -> analyze( | ||
| "FROM remote:my-index | LOOKUP JOIN languages_lookup ON language_code", | ||
|
||
| AnalyzerTestUtils.analyzer(remoteIndexResolution, lookupResolution) | ||
| ) | ||
| ); | ||
| assertThat( | ||
| e.getMessage(), | ||
| containsString("1:24: LOOKUP JOIN does not support joining with remote cluster indices [remote:my-index]") | ||
| ); | ||
| } | ||
|
|
||
| public void testImplicitCasting() { | ||
| var e = expectThrows(VerificationException.class, () -> analyze(""" | ||
| from test | eval x = concat("2024", "-04", "-01") + 1 day | ||
|
|
||
|
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. One more test case suggestion: comma-separated list of index patterns, some of which are remote. E.g. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -298,18 +298,18 @@ public void testStatsWithoutGroups() { | |
| ); | ||
| } | ||
|
|
||
| public void testStatsWithoutAggs() throws Exception { | ||
| public void testStatsWithoutAggs() { | ||
| assertEquals( | ||
| new Aggregate(EMPTY, PROCESSING_CMD_INPUT, Aggregate.AggregateType.STANDARD, List.of(attribute("a")), List.of(attribute("a"))), | ||
| processingCommand("stats by a") | ||
| ); | ||
| } | ||
|
|
||
| public void testStatsWithoutAggsOrGroup() throws Exception { | ||
| public void testStatsWithoutAggsOrGroup() { | ||
| expectError("from text | stats", "At least one aggregation or grouping expression required in [stats]"); | ||
| } | ||
|
|
||
| public void testAggsWithGroupKeyAsAgg() throws Exception { | ||
| public void testAggsWithGroupKeyAsAgg() { | ||
| var queries = new String[] { """ | ||
| row a = 1, b = 2 | ||
| | stats a by a | ||
|
|
@@ -330,7 +330,7 @@ public void testAggsWithGroupKeyAsAgg() throws Exception { | |
| } | ||
| } | ||
|
|
||
| public void testStatsWithGroupKeyAndAggFilter() throws Exception { | ||
| public void testStatsWithGroupKeyAndAggFilter() { | ||
| var a = attribute("a"); | ||
| var f = new UnresolvedFunction(EMPTY, "min", DEFAULT, List.of(a)); | ||
| var filter = new Alias(EMPTY, "min(a) where a > 1", new FilteredExpression(EMPTY, f, new GreaterThan(EMPTY, a, integer(1)))); | ||
|
|
@@ -340,7 +340,7 @@ public void testStatsWithGroupKeyAndAggFilter() throws Exception { | |
| ); | ||
| } | ||
|
|
||
| public void testStatsWithGroupKeyAndMixedAggAndFilter() throws Exception { | ||
| public void testStatsWithGroupKeyAndMixedAggAndFilter() { | ||
| var a = attribute("a"); | ||
| var min = new UnresolvedFunction(EMPTY, "min", DEFAULT, List.of(a)); | ||
| var max = new UnresolvedFunction(EMPTY, "max", DEFAULT, List.of(a)); | ||
|
|
@@ -375,7 +375,7 @@ public void testStatsWithGroupKeyAndMixedAggAndFilter() throws Exception { | |
| ); | ||
| } | ||
|
|
||
| public void testStatsWithoutGroupKeyMixedAggAndFilter() throws Exception { | ||
| public void testStatsWithoutGroupKeyMixedAggAndFilter() { | ||
| var a = attribute("a"); | ||
| var f = new UnresolvedFunction(EMPTY, "min", DEFAULT, List.of(a)); | ||
| var filter = new Alias(EMPTY, "min(a) where a > 1", new FilteredExpression(EMPTY, f, new GreaterThan(EMPTY, a, integer(1)))); | ||
|
|
@@ -2058,41 +2058,41 @@ private void assertStringAsLookupIndexPattern(String string, String statement) { | |
| assertThat(tableName.fold(FoldContext.small()), equalTo(string)); | ||
| } | ||
|
|
||
| public void testIdPatternUnquoted() throws Exception { | ||
| public void testIdPatternUnquoted() { | ||
| var string = "regularString"; | ||
| assertThat(breakIntoFragments(string), contains(string)); | ||
| } | ||
|
|
||
| public void testIdPatternQuoted() throws Exception { | ||
| public void testIdPatternQuoted() { | ||
| var string = "`escaped string`"; | ||
| assertThat(breakIntoFragments(string), contains(string)); | ||
| } | ||
|
|
||
| public void testIdPatternQuotedWithDoubleBackticks() throws Exception { | ||
| public void testIdPatternQuotedWithDoubleBackticks() { | ||
| var string = "`escaped``string`"; | ||
| assertThat(breakIntoFragments(string), contains(string)); | ||
| } | ||
|
|
||
| public void testIdPatternUnquotedAndQuoted() throws Exception { | ||
| public void testIdPatternUnquotedAndQuoted() { | ||
| var string = "this`is`a`mix`of`ids`"; | ||
| assertThat(breakIntoFragments(string), contains("this", "`is`", "a", "`mix`", "of", "`ids`")); | ||
| } | ||
|
|
||
| public void testIdPatternQuotedTraling() throws Exception { | ||
| public void testIdPatternQuotedTraling() { | ||
| var string = "`foo`*"; | ||
| assertThat(breakIntoFragments(string), contains("`foo`", "*")); | ||
| } | ||
|
|
||
| public void testIdPatternWithDoubleQuotedStrings() throws Exception { | ||
| public void testIdPatternWithDoubleQuotedStrings() { | ||
| var string = "`this``is`a`quoted `` string``with`backticks"; | ||
| assertThat(breakIntoFragments(string), contains("`this``is`", "a", "`quoted `` string``with`", "backticks")); | ||
| } | ||
|
|
||
| public void testSpaceNotAllowedInIdPattern() throws Exception { | ||
| public void testSpaceNotAllowedInIdPattern() { | ||
| expectError("ROW a = 1| RENAME a AS this is `not okay`", "mismatched input 'is' expecting {<EOF>, '|', ',', '.'}"); | ||
| } | ||
|
|
||
| public void testSpaceNotAllowedInIdPatternKeep() throws Exception { | ||
| public void testSpaceNotAllowedInIdPatternKeep() { | ||
| expectError("ROW a = 1, b = 1| KEEP a b", "extraneous input 'b'"); | ||
| } | ||
|
|
||
|
|
@@ -2358,4 +2358,9 @@ public void testFailingMetadataWithSquareBrackets() { | |
| "line 1:11: mismatched input '[' expecting {<EOF>, '|', ',', 'metadata'}" | ||
| ); | ||
| } | ||
|
|
||
| public void testInvalidRemoteLookupJoin() { | ||
| // TODO ES-10559 this should be replaced with a proper error message once grammar allows indexPattern as joinTarget | ||
| expectError("FROM my-index | LOOKUP JOIN remote:languages_lookup ON language_code", "line 1:35: token recognition error at: ':'"); | ||
|
||
| } | ||
| } | ||
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.
This is too specific. I think it will not work in case of queries like
Because the join's left child will not be an
EsRelation. TheEsRelationcan be many levels down in the tree.I'd approach this differently:
EsRelation.IndexMode.LOOKUP, a lookup join happens.EsRelationsmustn't be remote. We can just iterate over allEsRelations and check their concrete indices.Since my suggestion is a more global approach, I'd place this in the
Verifieras a separate verification step, rather than here in theAnalyzer. Reason: there can be multipleLOOKUP JOINs and we'll be re-checking theEsRelationin theFROMclause every time.We have tools to easily traverse query plans, collect nodes that are instanceof a certain class, and I think also getting only the plan's leaves. Take a look at the
transformDownmethods and similar methods in the same class. You can also take a peek atReplaceMissingFieldWithNullwhich needs to scan the plan for lookups as well.