Skip to content

Commit bfa21b5

Browse files
authored
For empty mappings use a LocalRelation (#105081)
Fixes #104809 by converting a plan to a local relation when there is no mapping for the index pattern.
1 parent 6cf9258 commit bfa21b5

File tree

11 files changed

+152
-19
lines changed

11 files changed

+152
-19
lines changed

docs/changelog/105081.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 105081
2+
summary: For empty mappings use a `LocalRelation`
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 104809

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -1139,12 +1139,13 @@ FROM employees
11391139
| STATS x = CONCAT(TO_STRING(ROUND(AVG(salary % 3))), TO_STRING(MAX(emp_no))),
11401140
y = ROUND((MIN(emp_no / 3) + PI() - MEDIAN(salary))/E())
11411141
BY z = languages % 2
1142+
| SORT z
11421143
;
11431144

11441145
x:s | y:d | z:i
1145-
1.010029 | -16452.0 | null
11461146
1.010100 | -15260.0 | 0
11471147
1.010097 | -16701.0 | 1
1148+
1.010029 | -16452.0 | null
11481149
;
11491150

11501151
nestedAggsOverGroupingWithAlias#[skip:-8.12.99,reason:supported in 8.13]

x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java

+84
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.Build;
1111
import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
1212
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
13+
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder;
1314
import org.elasticsearch.action.bulk.BulkRequestBuilder;
1415
import org.elasticsearch.action.index.IndexRequest;
1516
import org.elasticsearch.action.index.IndexRequestBuilder;
@@ -1413,6 +1414,89 @@ public void testCountTextField() {
14131414
}
14141415
}
14151416

1417+
public void testQueryOnEmptyMappingIndex() {
1418+
createIndex("empty-test", Settings.EMPTY);
1419+
createIndex("empty-test2", Settings.EMPTY);
1420+
IndicesAliasesRequestBuilder indicesAliasesRequestBuilder = indicesAdmin().prepareAliases()
1421+
.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("empty-test").alias("alias-test"))
1422+
.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("empty-test2").alias("alias-test"));
1423+
indicesAdmin().aliases(indicesAliasesRequestBuilder.request()).actionGet();
1424+
1425+
String[] indexPatterns = new String[] { "empty-test", "empty-test,empty-test2", "empty-test*", "alias-test", "*-test*" };
1426+
String from = "FROM " + randomFrom(indexPatterns) + " ";
1427+
1428+
assertEmptyIndexQueries(from);
1429+
1430+
try (EsqlQueryResponse resp = run(from + "[METADATA _source] | EVAL x = 123")) {
1431+
assertFalse(resp.values().hasNext());
1432+
assertThat(resp.columns(), equalTo(List.of(new ColumnInfo("_source", "_source"), new ColumnInfo("x", "integer"))));
1433+
}
1434+
1435+
try (EsqlQueryResponse resp = run(from)) {
1436+
assertFalse(resp.values().hasNext());
1437+
assertThat(resp.columns(), equalTo(List.of(new ColumnInfo("<no-fields>", "null"))));
1438+
}
1439+
}
1440+
1441+
public void testQueryOnEmptyDataIndex() {
1442+
createIndex("empty_data-test", Settings.EMPTY);
1443+
assertAcked(client().admin().indices().prepareCreate("empty_data-test2").setMapping("name", "type=text"));
1444+
IndicesAliasesRequestBuilder indicesAliasesRequestBuilder = indicesAdmin().prepareAliases()
1445+
.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("empty_data-test").alias("alias-empty_data-test"))
1446+
.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("empty_data-test2").alias("alias-empty_data-test"));
1447+
indicesAdmin().aliases(indicesAliasesRequestBuilder.request()).actionGet();
1448+
1449+
String[] indexPatterns = new String[] {
1450+
"empty_data-test2",
1451+
"empty_data-test,empty_data-test2",
1452+
"alias-empty_data-test",
1453+
"*data-test" };
1454+
String from = "FROM " + randomFrom(indexPatterns) + " ";
1455+
1456+
assertEmptyIndexQueries(from);
1457+
1458+
try (EsqlQueryResponse resp = run(from + "[METADATA _source] | EVAL x = 123")) {
1459+
assertFalse(resp.values().hasNext());
1460+
assertThat(
1461+
resp.columns(),
1462+
equalTo(List.of(new ColumnInfo("name", "text"), new ColumnInfo("_source", "_source"), new ColumnInfo("x", "integer")))
1463+
);
1464+
}
1465+
1466+
try (EsqlQueryResponse resp = run(from)) {
1467+
assertFalse(resp.values().hasNext());
1468+
assertThat(resp.columns(), equalTo(List.of(new ColumnInfo("name", "text"))));
1469+
}
1470+
}
1471+
1472+
private void assertEmptyIndexQueries(String from) {
1473+
try (EsqlQueryResponse resp = run(from + "[METADATA _source] | KEEP _source | LIMIT 1")) {
1474+
assertFalse(resp.values().hasNext());
1475+
assertThat(resp.columns(), equalTo(List.of(new ColumnInfo("_source", "_source"))));
1476+
}
1477+
1478+
try (EsqlQueryResponse resp = run(from + "| EVAL y = 1 | KEEP y | LIMIT 1 | EVAL x = 1")) {
1479+
assertFalse(resp.values().hasNext());
1480+
assertThat(resp.columns(), equalTo(List.of(new ColumnInfo("y", "integer"), new ColumnInfo("x", "integer"))));
1481+
}
1482+
1483+
try (EsqlQueryResponse resp = run(from + "| STATS c = count()")) {
1484+
assertTrue(resp.values().hasNext());
1485+
Iterator<Object> row = resp.values().next();
1486+
assertThat(row.next(), equalTo((long) 0));
1487+
assertThat(resp.columns(), equalTo(List.of(new ColumnInfo("c", "long"))));
1488+
}
1489+
1490+
try (EsqlQueryResponse resp = run(from + "| STATS c = count() | EVAL x = 123")) {
1491+
assertTrue(resp.values().hasNext());
1492+
Iterator<Object> row = resp.values().next();
1493+
assertThat(row.next(), equalTo((long) 0));
1494+
assertThat(row.next(), equalTo(123));
1495+
assertFalse(row.hasNext());
1496+
assertThat(resp.columns(), equalTo(List.of(new ColumnInfo("c", "long"), new ColumnInfo("x", "integer"))));
1497+
}
1498+
}
1499+
14161500
private void createNestedMappingIndex(String indexName) throws IOException {
14171501
XContentBuilder builder = JsonXContent.contentBuilder();
14181502
builder.startObject();

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@
8383
import static org.elasticsearch.xpack.ql.type.DataTypes.NESTED;
8484

8585
public class Analyzer extends ParameterizedRuleExecutor<LogicalPlan, AnalyzerContext> {
86-
static final List<Attribute> NO_FIELDS = List.of(
87-
new ReferenceAttribute(Source.EMPTY, "<no-fields>", DataTypes.NULL, null, Nullability.TRUE, null, false)
86+
// marker list of attributes for plans that do not have any concrete fields to return, but have other computed columns to return
87+
// ie from test | stats c = count(*)
88+
public static final List<Attribute> NO_FIELDS = List.of(
89+
new ReferenceAttribute(Source.EMPTY, "<no-fields>", DataTypes.NULL, null, Nullability.TRUE, null, true)
8890
);
8991
private static final Iterable<RuleExecutor.Batch<LogicalPlan>> rules;
9092

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ protected static List<Batch<LogicalPlan>> rules() {
152152
// lastly replace surrogate functions
153153
new SubstituteSurrogates(),
154154
new ReplaceRegexMatch(),
155-
new ReplaceAliasingEvalWithProject()
155+
new ReplaceAliasingEvalWithProject(),
156+
new SkipQueryOnEmptyMappings()
156157
// new NormalizeAggregate(), - waits on https://github.com/elastic/elasticsearch/issues/100634
157158
);
158159

@@ -704,6 +705,14 @@ protected LogicalPlan rule(UnaryPlan plan) {
704705
}
705706
}
706707

708+
static class SkipQueryOnEmptyMappings extends OptimizerRules.OptimizerRule<EsRelation> {
709+
710+
@Override
711+
protected LogicalPlan rule(EsRelation plan) {
712+
return plan.index().concreteIndices().isEmpty() ? new LocalRelation(plan.source(), plan.output(), LocalSupplier.EMPTY) : plan;
713+
}
714+
}
715+
707716
@SuppressWarnings("removal")
708717
static class PropagateEmptyRelation extends OptimizerRules.OptimizerRule<UnaryPlan> {
709718

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.List;
4040
import java.util.Locale;
4141
import java.util.Map;
42+
import java.util.Set;
4243

4344
import static java.util.Collections.emptyMap;
4445
import static org.elasticsearch.xpack.esql.EsqlTestUtils.L;
@@ -69,7 +70,7 @@ public static void init() {
6970
parser = new EsqlParser();
7071

7172
mapping = loadMapping("mapping-basic.json");
72-
EsIndex test = new EsIndex("test", mapping);
73+
EsIndex test = new EsIndex("test", mapping, Set.of("test"));
7374
IndexResolution getIndexResult = IndexResolution.valid(test);
7475
logicalOptimizer = new LogicalPlanOptimizer(new LogicalOptimizerContext(EsqlTestUtils.TEST_CFG));
7576

@@ -321,7 +322,7 @@ public void testSparseDocument() throws Exception {
321322

322323
SearchStats searchStats = statsForExistingField("field000", "field001", "field002", "field003", "field004");
323324

324-
EsIndex index = new EsIndex("large", large);
325+
EsIndex index = new EsIndex("large", large, Set.of("large"));
325326
IndexResolution getIndexResult = IndexResolution.valid(index);
326327
var logicalOptimizer = new LogicalPlanOptimizer(new LogicalOptimizerContext(EsqlTestUtils.TEST_CFG));
327328

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import java.util.ArrayList;
5959
import java.util.List;
6060
import java.util.Map;
61+
import java.util.Set;
6162
import java.util.stream.Collectors;
6263

6364
import static java.util.Arrays.asList;
@@ -143,7 +144,7 @@ public void init() {
143144

144145
private Analyzer makeAnalyzer(String mappingFileName, EnrichResolution enrichResolution) {
145146
var mapping = loadMapping(mappingFileName);
146-
EsIndex test = new EsIndex("test", mapping);
147+
EsIndex test = new EsIndex("test", mapping, Set.of("test"));
147148
IndexResolution getIndexResult = IndexResolution.valid(test);
148149

149150
return new Analyzer(new AnalyzerContext(config, functionRegistry, getIndexResult, enrichResolution), new Verifier(new Metrics()));

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java

+35-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.elasticsearch.xpack.esql.analysis.Analyzer;
1616
import org.elasticsearch.xpack.esql.analysis.AnalyzerContext;
1717
import org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils;
18+
import org.elasticsearch.xpack.esql.analysis.EnrichResolution;
1819
import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.Equals;
1920
import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.GreaterThan;
2021
import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.GreaterThanOrEqual;
@@ -85,9 +86,11 @@
8586

8687
import java.util.List;
8788
import java.util.Map;
89+
import java.util.Set;
8890

8991
import static java.util.Collections.emptyList;
9092
import static java.util.Collections.emptyMap;
93+
import static java.util.Collections.emptySet;
9194
import static java.util.Collections.singletonList;
9295
import static org.elasticsearch.xpack.esql.EsqlTestUtils.L;
9396
import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_VERIFIER;
@@ -96,6 +99,7 @@
9699
import static org.elasticsearch.xpack.esql.EsqlTestUtils.loadMapping;
97100
import static org.elasticsearch.xpack.esql.EsqlTestUtils.localSource;
98101
import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning;
102+
import static org.elasticsearch.xpack.esql.analysis.Analyzer.NO_FIELDS;
99103
import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_POINT;
100104
import static org.elasticsearch.xpack.ql.TestUtils.relation;
101105
import static org.elasticsearch.xpack.ql.tree.Source.EMPTY;
@@ -124,21 +128,17 @@ public class LogicalPlanOptimizerTests extends ESTestCase {
124128
private static Map<String, EsField> mapping;
125129
private static Map<String, EsField> mappingAirports;
126130
private static Analyzer analyzerAirports;
131+
private static EnrichResolution enrichResolution;
127132

128133
@BeforeClass
129134
public static void init() {
130135
parser = new EsqlParser();
131136
logicalOptimizer = new LogicalPlanOptimizer(new LogicalOptimizerContext(EsqlTestUtils.TEST_CFG));
132-
var enrichResolution = AnalyzerTestUtils.loadEnrichPolicyResolution(
133-
"languages_idx",
134-
"id",
135-
"languages_idx",
136-
"mapping-languages.json"
137-
);
137+
enrichResolution = AnalyzerTestUtils.loadEnrichPolicyResolution("languages_idx", "id", "languages_idx", "mapping-languages.json");
138138

139139
// Most tests used data from the test index, so we load it here, and use it in the plan() function.
140140
mapping = loadMapping("mapping-basic.json");
141-
EsIndex test = new EsIndex("test", mapping);
141+
EsIndex test = new EsIndex("test", mapping, Set.of("test"));
142142
IndexResolution getIndexResult = IndexResolution.valid(test);
143143
analyzer = new Analyzer(
144144
new AnalyzerContext(EsqlTestUtils.TEST_CFG, new EsqlFunctionRegistry(), getIndexResult, enrichResolution),
@@ -147,7 +147,7 @@ public static void init() {
147147

148148
// Some tests use data from the airports index, so we load it here, and use it in the plan_airports() function.
149149
mappingAirports = loadMapping("mapping-airports.json");
150-
EsIndex airports = new EsIndex("airports", mappingAirports);
150+
EsIndex airports = new EsIndex("airports", mappingAirports, Set.of("airports"));
151151
IndexResolution getIndexResultAirports = IndexResolution.valid(airports);
152152
analyzerAirports = new Analyzer(
153153
new AnalyzerContext(EsqlTestUtils.TEST_CFG, new EsqlFunctionRegistry(), getIndexResultAirports, enrichResolution),
@@ -3182,6 +3182,33 @@ public void testStatsWithCanonicalAggregate() throws Exception {
31823182
assertThat(Expressions.attribute(fields.get(1)), is(Expressions.attribute(sum_argument)));
31833183
}
31843184

3185+
public void testEmptyMappingIndex() {
3186+
EsIndex empty = new EsIndex("empty_test", emptyMap(), emptySet());
3187+
IndexResolution getIndexResultAirports = IndexResolution.valid(empty);
3188+
var analyzer = new Analyzer(
3189+
new AnalyzerContext(EsqlTestUtils.TEST_CFG, new EsqlFunctionRegistry(), getIndexResultAirports, enrichResolution),
3190+
TEST_VERIFIER
3191+
);
3192+
3193+
var plan = logicalOptimizer.optimize(analyzer.analyze(parser.createStatement("from empty_test")));
3194+
as(plan, LocalRelation.class);
3195+
assertThat(plan.output(), equalTo(NO_FIELDS));
3196+
3197+
plan = logicalOptimizer.optimize(analyzer.analyze(parser.createStatement("from empty_test [metadata _id] | eval x = 1")));
3198+
as(plan, LocalRelation.class);
3199+
assertThat(Expressions.names(plan.output()), contains("_id", "x"));
3200+
3201+
plan = logicalOptimizer.optimize(analyzer.analyze(parser.createStatement("from empty_test [metadata _id, _version] | limit 5")));
3202+
as(plan, LocalRelation.class);
3203+
assertThat(Expressions.names(plan.output()), contains("_id", "_version"));
3204+
3205+
plan = logicalOptimizer.optimize(
3206+
analyzer.analyze(parser.createStatement("from empty_test | eval x = \"abc\" | enrich languages_idx on x"))
3207+
);
3208+
LocalRelation local = as(plan, LocalRelation.class);
3209+
assertThat(Expressions.names(local.output()), contains(NO_FIELDS.get(0).name(), "x", "language_code", "language_name"));
3210+
}
3211+
31853212
private LogicalPlan optimizedPlan(String query) {
31863213
return plan(query);
31873214
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public void init() {
176176
mapper = new Mapper(functionRegistry);
177177
// Most tests used data from the test index, so we load it here, and use it in the plan() function.
178178
mapping = loadMapping("mapping-basic.json");
179-
EsIndex test = new EsIndex("test", mapping);
179+
EsIndex test = new EsIndex("test", mapping, Set.of("test"));
180180
IndexResolution getIndexResult = IndexResolution.valid(test);
181181
var enrichResolution = setupEnrichResolution();
182182
analyzer = new Analyzer(new AnalyzerContext(config, functionRegistry, getIndexResult, enrichResolution), TEST_VERIFIER);
@@ -194,7 +194,7 @@ public void init() {
194194

195195
// Some tests use data from the airports index, so we load it here, and use it in the plan_airports() function.
196196
mappingAirports = loadMapping("mapping-airports.json");
197-
EsIndex airports = new EsIndex("airports", mappingAirports);
197+
EsIndex airports = new EsIndex("airports", mappingAirports, Set.of("airports"));
198198
IndexResolution getIndexResultAirports = IndexResolution.valid(airports);
199199
analyzerAirports = new Analyzer(
200200
new AnalyzerContext(config, functionRegistry, getIndexResultAirports, enrichResolution),

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/FilterTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.io.UncheckedIOException;
4242
import java.util.List;
4343
import java.util.Map;
44+
import java.util.Set;
4445

4546
import static java.util.Arrays.asList;
4647
import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
@@ -72,7 +73,7 @@ public static void init() {
7273
parser = new EsqlParser();
7374

7475
mapping = loadMapping("mapping-basic.json");
75-
EsIndex test = new EsIndex("test", mapping);
76+
EsIndex test = new EsIndex("test", mapping, Set.of("test"));
7677
IndexResolution getIndexResult = IndexResolution.valid(test);
7778
logicalOptimizer = new LogicalPlanOptimizer(new LogicalOptimizerContext(EsqlTestUtils.TEST_CFG));
7879
physicalPlanOptimizer = new PhysicalPlanOptimizer(new PhysicalOptimizerContext(EsqlTestUtils.TEST_CFG));

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/DataNodeRequestTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.io.IOException;
3838
import java.util.List;
3939
import java.util.Map;
40+
import java.util.Set;
4041

4142
import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG;
4243
import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_VERIFIER;
@@ -208,7 +209,7 @@ protected DataNodeRequest mutateInstance(DataNodeRequest in) throws IOException
208209

209210
static LogicalPlan parse(String query) {
210211
Map<String, EsField> mapping = loadMapping("mapping-basic.json");
211-
EsIndex test = new EsIndex("test", mapping);
212+
EsIndex test = new EsIndex("test", mapping, Set.of("test"));
212213
IndexResolution getIndexResult = IndexResolution.valid(test);
213214
var logicalOptimizer = new LogicalPlanOptimizer(new LogicalOptimizerContext(TEST_CFG));
214215
var analyzer = new Analyzer(

0 commit comments

Comments
 (0)