Skip to content

Commit 2af97f4

Browse files
authored
ESQL: Fix projection generation when pruning left join (#135446)
Currently, in case the join key is (locally) null, the whole join is replaced with an `Eval` and a `Project`. The project however contain only reference attributes. This is insufficient, in case the attribute should point to another attribute pointing to a literal `null` added in the `Eval`. This fixes that by allowing the generated `Project` to contain `NamedExpression`s, which can then be proper `Alias`es. This is done by adjusting the join output calculation to work with the `NamedExpression`s, instead of just `Attribute`s.
1 parent d3894ca commit 2af97f4

File tree

15 files changed

+207
-33
lines changed

15 files changed

+207
-33
lines changed

docs/changelog/135446.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 135446
2+
summary: Fix projection generation when pruning left join
3+
area: ES|QL
4+
type: bug
5+
issues: []

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/AttributeMap.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,12 @@ public static <E> AttributeMap<E> of(Attribute key, E value) {
393393
return map;
394394
}
395395

396+
public static <E> AttributeMap<E> mapAll(Collection<? extends E> collection, Function<E, Attribute> keyMapper) {
397+
final AttributeMap<E> map = new AttributeMap<>();
398+
collection.forEach(e -> map.add(keyMapper.apply(e), e));
399+
return map;
400+
}
401+
396402
public static <E> Builder<E> builder() {
397403
return new Builder<>(new AttributeMap<>());
398404
}

x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/AttributeMapTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,18 @@ public void testValuesIteratorRemoval() {
344344
it.remove();
345345
assertThat(it.hasNext(), is(false));
346346
}
347+
348+
public void testMappAll() {
349+
var one = a("one");
350+
var two = a("two");
351+
var three = a("three");
352+
353+
Collection<Attribute> collection = asList(one, two, three);
354+
var map = AttributeMap.mapAll(collection, NamedExpression::toAttribute);
355+
356+
var builder = AttributeMap.builder();
357+
collection.forEach(e -> builder.put(e, e.toAttribute()));
358+
359+
assertThat(map, is(builder.build()));
360+
}
347361
}

x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,16 @@ public abstract class RestEsqlTestCase extends ESRestTestCase {
8989

9090
private static final Logger LOGGER = LogManager.getLogger(RestEsqlTestCase.class);
9191

92+
private static final String MAPPING_FIELD;
9293
private static final String MAPPING_ALL_TYPES;
93-
9494
private static final String MAPPING_ALL_TYPES_LOOKUP;
9595

9696
static {
9797
String properties = EsqlTestUtils.loadUtf8TextFile("/mapping-all-types.json");
98-
MAPPING_ALL_TYPES = "{\"mappings\": " + properties + "}";
99-
String settings = "{\"settings\" : {\"mode\" : \"lookup\"}";
100-
MAPPING_ALL_TYPES_LOOKUP = settings + ", " + "\"mappings\": " + properties + "}";
98+
MAPPING_FIELD = "\"mappings\": " + properties;
99+
MAPPING_ALL_TYPES = "{" + MAPPING_FIELD + "}";
100+
String settings = "\"settings\" : {\"mode\" : \"lookup\"}";
101+
MAPPING_ALL_TYPES_LOOKUP = "{" + settings + ", " + MAPPING_FIELD + "}";
101102
}
102103

103104
private static final String DOCUMENT_TEMPLATE = """
@@ -1154,6 +1155,24 @@ public void testMultipleBatchesWithLookupJoin() throws IOException {
11541155
}
11551156
}
11561157

1158+
public void testPruneLeftJoinOnNullMatchingFieldAndShadowingAttributes() throws IOException {
1159+
var standardIndexName = "standard";
1160+
createIndex(standardIndexName, false, MAPPING_FIELD);
1161+
createIndex(testIndexName(), true);
1162+
1163+
var query = format(
1164+
null,
1165+
"FROM {}* | EVAL keyword = null::KEYWORD | LOOKUP JOIN {} ON keyword | KEEP keyword, integer, alias_integer | SORT keyword",
1166+
standardIndexName,
1167+
testIndexName()
1168+
);
1169+
Map<String, Object> result = runEsql(requestObjectBuilder().query(query));
1170+
var values = as(result.get("values"), List.class);
1171+
assertThat(values.size(), is(0));
1172+
1173+
assertThat(deleteIndex(standardIndexName).isAcknowledged(), is(true));
1174+
}
1175+
11571176
public void testErrorMessageForLiteralDateMathOverflow() throws IOException {
11581177
List<String> dateMathOverflowExpressions = List.of(
11591178
"2147483647 day + 1 day",

x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ public class CsvTestsDataLoader {
7777
).withSetting("lookup-settings.json");
7878
private static final TestDataset LANGUAGES = new TestDataset("languages");
7979
private static final TestDataset LANGUAGES_LOOKUP = LANGUAGES.withIndex("languages_lookup").withSetting("lookup-settings.json");
80-
private static final TestDataset LANGUAGES_LOOKUP_NON_UNIQUE_KEY = LANGUAGES_LOOKUP.withIndex("languages_lookup_non_unique_key")
81-
.withData("languages_non_unique_key.csv");
80+
private static final TestDataset LANGUAGES_NON_UNIQUE_KEY = new TestDataset("languages_non_unique_key");
81+
private static final TestDataset LANGUAGES_LOOKUP_NON_UNIQUE_KEY = LANGUAGES_NON_UNIQUE_KEY.withIndex("languages_lookup_non_unique_key")
82+
.withSetting("lookup-settings.json");
8283
private static final TestDataset LANGUAGES_NESTED_FIELDS = new TestDataset(
8384
"languages_nested_fields",
8485
"mapping-languages_nested_fields.json",

x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,11 @@ public static Range rangeOf(Expression value, Expression lower, boolean includeL
260260
}
261261

262262
public static EsRelation relation() {
263-
return new EsRelation(EMPTY, new EsIndex(randomAlphaOfLength(8), emptyMap()), IndexMode.STANDARD);
263+
return relation(IndexMode.STANDARD);
264+
}
265+
266+
public static EsRelation relation(IndexMode mode) {
267+
return new EsRelation(EMPTY, new EsIndex(randomAlphaOfLength(8), emptyMap()), mode);
264268
}
265269

266270
/**

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3970,3 +3970,23 @@ FROM k8s-downsampled
39703970
2024-05-09T23:30:00.000Z | staging | three | {"min":341.0,"max":592.0,"sum":1956.0,"value_count":5} | 824.0
39713971
2024-05-09T23:30:00.000Z | staging | two | {"min":442.0,"max":1011.0,"sum":3850.0,"value_count":7} | 1419.0
39723972
;
3973+
3974+
selfShadowing
3975+
required_capability: inline_stats
3976+
required_capability: fix_join_output_merging
3977+
3978+
FROM languages_lookup_non_unique_key
3979+
| KEEP country, language_name
3980+
| EVAL language_code = null::integer
3981+
| INLINE STATS MAX(language_code) BY language_code
3982+
| SORT country
3983+
| LIMIT 5
3984+
;
3985+
3986+
country:text |language_name:keyword |MAX(language_code):integer |language_code:integer
3987+
Atlantis |null |null |null
3988+
[Austria, Germany]|German |null |null
3989+
Canada |English |null |null
3990+
Mv-Land |Mv-Lang |null |null
3991+
Mv-Land2 |Mv-Lang2 |null |null
3992+
;

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,22 @@ language_code:integer | language_name:keyword
136136
6 |null
137137
;
138138

139+
selfShadowing
140+
required_capability: join_lookup_v12
141+
required_capability: fix_join_output_merging
142+
143+
FROM languages_lookup_non_unique_key
144+
| EVAL language_code = null::integer
145+
| LOOKUP JOIN languages_lookup_non_unique_key ON language_code
146+
| LIMIT 3
147+
;
148+
149+
language_code:integer |country:text |country.keyword:keyword |language_name:keyword
150+
null |null |null |null
151+
null |null |null |null
152+
null |null |null |null
153+
;
154+
139155
nonUniqueLeftKeyOnTheDataNode
140156
required_capability: join_lookup_v12
141157

@@ -240,7 +256,7 @@ emp_no:integer | language_code:integer | language_name:keyword | country:text
240256
10091 | 1 | null | United Kingdom
241257
10091 | 1 | English | United States of America
242258
10091 | 1 | English | null
243-
10092 | 2 | German | [Germany, Austria]
259+
10092 | 2 | German | [Austria, Germany]
244260
10092 | 2 | German | Switzerland
245261
10092 | 2 | German | null
246262
10093 | 3 | null | null
@@ -265,7 +281,7 @@ emp_no:integer | language_code:integer | language_name:keyword | country:text
265281
10001 | 1 | English | null
266282
10001 | 1 | null | United Kingdom
267283
10001 | 1 | English | United States of America
268-
10002 | 2 | German | [Germany, Austria]
284+
10002 | 2 | German | [Austria, Germany]
269285
10002 | 2 | German | Switzerland
270286
10002 | 2 | German | null
271287
10003 | 3 | null | null
@@ -308,7 +324,7 @@ ROW language_code = 2
308324

309325
ignoreOrder:true
310326
language_code:integer | country:text | language_name:keyword
311-
2 | [Germany, Austria] | German
327+
2 | [Austria, Germany] | German
312328
2 | Switzerland | German
313329
2 | null | German
314330
;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"properties" : {
3+
"language_code" : {
4+
"type" : "integer"
5+
},
6+
"language_name" : {
7+
"type" : "keyword"
8+
},
9+
"country": {
10+
"type": "text",
11+
"fields": {
12+
"keyword": {
13+
"type": "keyword"
14+
}
15+
}
16+
}
17+
}
18+
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,11 @@ public enum Cap {
11571157
*/
11581158
FIX_JOIN_MASKING_REGEX_EXTRACT,
11591159

1160+
/**
1161+
* Allow the merging of the children to use {@code Aliase}s, instead of just {@code ReferenceAttribute}s.
1162+
*/
1163+
FIX_JOIN_OUTPUT_MERGING,
1164+
11601165
/**
11611166
* Avid GROK and DISSECT attributes being removed when resolving fields.
11621167
* see <a href="https://github.com/elastic/elasticsearch/issues/127468"> ES|QL: Grok only supports KEYWORD or TEXT values, found expression [type] type [INTEGER] #127468 </a>

0 commit comments

Comments
 (0)