Browse Source

Make ESQL more resilient to non-indexed fields (#99588)

Fixes https://github.com/elastic/elasticsearch/issues/99506

This is an attempt to avoid runtime exceptions when dealing with fields
that have no `doc_values` support (eg. that are not indexed) and then
cannot be extracted. In this scenario, we currently support extraction
of keyword fields and of text fields when source is present (ie. not
with synthetic source), but for all the other types, ESQL fails with an
exception.

This PR adds a last resort defense against these errors, returning a
default value when actual values cannot be extracted, and avoiding
runtime failures.

There is a significant change here that impacts unsupported fields:
before this PR, unsupported field values where returned as
`"<unsupported>"`. We cannot use this value also in this case, because
at this stage the data type is well defined already, with all the
constraints in terms of block types, so we cannot always return a
BytesRef (eg. for numeric, that has different blocks, or IP, that
requires a specific string format). To avoid multiple ways of returning
invalid values, this PR uniforms it returning `null` in all cases AND
emitting a warning regarding the unsupported field.

This has a few advantages: - `null` is valid for all types - it doesn't
overlap with valid values (eg. `"<unsupported>"` is a valid value for a
KEYWORD field) - it's the only alternative for types where defining a
value for unsupported values is practically impossible, like for
numerics - keeps the result clean, moving the report of the problem to
the right place, that is warnings

There is an alternative to this approach, that is to try to intercept
the problem during the query analysis/resolution phase. It could be more
elegant, but we risk to miss some cases and still have to catch errors
during the physical/extraction/execution phase, so we'll probably need
this anyway.
Luigi Dell'Aquila 2 years ago
parent
commit
33d8604d86

+ 6 - 0
docs/changelog/99588.yaml

@@ -0,0 +1,6 @@
+pr: 99588
+summary: Make ESQL more resilient to non-indexed fields
+area: ES|QL
+type: bug
+issues:
+ - 99506

+ 3 - 19
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/UnsupportedValueSource.java

@@ -8,9 +8,9 @@
 package org.elasticsearch.compute.lucene;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.common.Rounding;
 import org.elasticsearch.index.fielddata.DocValueBits;
+import org.elasticsearch.index.fielddata.FieldData;
 import org.elasticsearch.index.fielddata.SortedBinaryDocValues;
 import org.elasticsearch.search.aggregations.support.AggregationContext;
 import org.elasticsearch.search.aggregations.support.ValuesSource;
@@ -20,8 +20,7 @@ import java.util.function.Function;
 
 public class UnsupportedValueSource extends ValuesSource {
 
-    public static final String UNSUPPORTED_OUTPUT = "<unsupported>";
-    private static final BytesRef result = new BytesRef(UNSUPPORTED_OUTPUT);
+    public static final String UNSUPPORTED_OUTPUT = null;
     private final ValuesSource originalSource;
 
     public UnsupportedValueSource(ValuesSource originalSource) {
@@ -37,22 +36,7 @@ public class UnsupportedValueSource extends ValuesSource {
                 // ignore and fall back to UNSUPPORTED_OUTPUT
             }
         }
-        return new SortedBinaryDocValues() {
-            @Override
-            public boolean advanceExact(int doc) throws IOException {
-                return true;
-            }
-
-            @Override
-            public int docValueCount() {
-                return 1;
-            }
-
-            @Override
-            public BytesRef nextValue() throws IOException {
-                return result;
-            }
-        };
+        return FieldData.emptySortedBinary();
     }
 
     @Override

+ 60 - 13
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValueSources.java

@@ -7,10 +7,16 @@
 
 package org.elasticsearch.compute.lucene;
 
+import org.apache.lucene.index.DocValues;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.SortedNumericDocValues;
 import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.logging.HeaderWarning;
 import org.elasticsearch.compute.data.ElementType;
 import org.elasticsearch.index.fielddata.FieldDataContext;
 import org.elasticsearch.index.fielddata.IndexFieldData;
+import org.elasticsearch.index.fielddata.SortedBinaryDocValues;
+import org.elasticsearch.index.fielddata.SortedNumericDoubleValues;
 import org.elasticsearch.index.fielddata.SourceValueFetcherSortedBinaryIndexFieldData;
 import org.elasticsearch.index.fielddata.StoredFieldSortedBinaryIndexFieldData;
 import org.elasticsearch.index.mapper.IdFieldMapper;
@@ -24,6 +30,7 @@ import org.elasticsearch.search.aggregations.support.FieldContext;
 import org.elasticsearch.search.aggregations.support.ValuesSource;
 import org.elasticsearch.search.internal.SearchContext;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -71,19 +78,9 @@ public final class ValueSources {
             try {
                 fieldData = ctx.getForField(fieldType, MappedFieldType.FielddataOperation.SEARCH);
             } catch (IllegalArgumentException e) {
-                if (asUnsupportedSource) {
-                    sources.add(
-                        new ValueSourceInfo(
-                            new UnsupportedValueSourceType(fieldType.typeName()),
-                            new UnsupportedValueSource(null),
-                            elementType,
-                            ctx.getIndexReader()
-                        )
-                    );
-                    continue;
-                } else {
-                    throw e;
-                }
+                sources.add(unsupportedValueSource(elementType, ctx, fieldType, e));
+                HeaderWarning.addWarning("Field [{}] cannot be retrieved, it is unsupported or not indexed; returning null", fieldName);
+                continue;
             }
             var fieldContext = new FieldContext(fieldName, fieldData, fieldType);
             var vsType = fieldData.getValuesSourceType();
@@ -106,6 +103,56 @@ public final class ValueSources {
         return sources;
     }
 
+    private static ValueSourceInfo unsupportedValueSource(
+        ElementType elementType,
+        SearchExecutionContext ctx,
+        MappedFieldType fieldType,
+        IllegalArgumentException e
+    ) {
+        return switch (elementType) {
+            case BYTES_REF -> new ValueSourceInfo(
+                new UnsupportedValueSourceType(fieldType.typeName()),
+                new UnsupportedValueSource(null),
+                elementType,
+                ctx.getIndexReader()
+            );
+            case LONG, INT -> new ValueSourceInfo(
+                CoreValuesSourceType.NUMERIC,
+                ValuesSource.Numeric.EMPTY,
+                elementType,
+                ctx.getIndexReader()
+            );
+            case BOOLEAN -> new ValueSourceInfo(
+                CoreValuesSourceType.BOOLEAN,
+                ValuesSource.Numeric.EMPTY,
+                elementType,
+                ctx.getIndexReader()
+            );
+            case DOUBLE -> new ValueSourceInfo(CoreValuesSourceType.NUMERIC, new ValuesSource.Numeric() {
+                @Override
+                public boolean isFloatingPoint() {
+                    return true;
+                }
+
+                @Override
+                public SortedNumericDocValues longValues(LeafReaderContext context) {
+                    return DocValues.emptySortedNumeric();
+                }
+
+                @Override
+                public SortedNumericDoubleValues doubleValues(LeafReaderContext context) throws IOException {
+                    return org.elasticsearch.index.fielddata.FieldData.emptySortedNumericDoubles();
+                }
+
+                @Override
+                public SortedBinaryDocValues bytesValues(LeafReaderContext context) throws IOException {
+                    return org.elasticsearch.index.fielddata.FieldData.emptySortedBinary();
+                }
+            }, elementType, ctx.getIndexReader());
+            default -> throw e;
+        };
+    }
+
     private static TextValueSource textValueSource(SearchExecutionContext ctx, MappedFieldType fieldType) {
         if (fieldType.isStored()) {
             IndexFieldData<?> fieldData = new StoredFieldSortedBinaryIndexFieldData(

+ 6 - 2
x-pack/plugin/esql/qa/server/single-node/src/yamlRestTest/resources/rest-api-spec/test/40_tsdb.yml

@@ -1,4 +1,6 @@
 setup:
+  - skip:
+      features: allowed_warnings_regex
   - do:
       indices.create:
           index: test
@@ -84,6 +86,8 @@ load everything:
 ---
 load a document:
   - do:
+      allowed_warnings_regex:
+        - "Field \\[.*\\] cannot be retrieved, it is unsupported or not indexed; returning null"
       esql.query:
         body:
           query: 'from test | where @timestamp == "2021-04-28T18:50:23.142Z"'
@@ -93,8 +97,8 @@ load a document:
   - match: {values.0.0: "2021-04-28T18:50:23.142Z"}
   - match: {values.0.1: "10.10.55.3"}
   - match: {values.0.2: "dog"}
-  - match: {values.0.3: "<unsupported>"}
-  - match: {values.0.4: "<unsupported>"}
+  - match: {values.0.3: null }
+  - match: {values.0.4: null }
   - match: {values.0.5: "df3145b3-0563-4d3b-a0f7-897eb2876ea9"}
   - match: {values.0.6: "pod"}
 

+ 29 - 23
x-pack/plugin/esql/qa/server/single-node/src/yamlRestTest/resources/rest-api-spec/test/40_unsupported_types.yml

@@ -1,5 +1,6 @@
----
-unsupported:
+setup:
+  - skip:
+      features: allowed_warnings_regex
   - do:
       indices.create:
         index: test
@@ -98,10 +99,15 @@ unsupported:
             "some_doc": { "foo": "xy", "bar": 12 }
           }
 
+---
+unsupported:
   - do:
+      allowed_warnings_regex:
+        - "Field \\[.*\\] cannot be retrieved, it is unsupported or not indexed; returning null"
       esql.query:
         body:
           query: 'from test'
+
   - match: { columns.0.name: aggregate_metric_double }
   - match: { columns.0.type: unsupported }
   - match: { columns.1.name: binary }
@@ -158,29 +164,29 @@ unsupported:
   - match: { columns.26.type: integer }
 
   - length: { values: 1 }
-  - match: { values.0.0: "<unsupported>" }
-  - match: { values.0.1: "<unsupported>" }
-  - match: { values.0.2: "<unsupported>" }
-  - match: { values.0.3: "<unsupported>" }
-  - match: { values.0.4: "<unsupported>" }
-  - match: { values.0.5: "<unsupported>" }
-  - match: { values.0.6: "<unsupported>" }
-  - match: { values.0.7: "<unsupported>" }
-  - match: { values.0.8: "<unsupported>" }
-  - match: { values.0.9: "<unsupported>" }
-  - match: { values.0.10: "<unsupported>" }
-  - match: { values.0.11: "<unsupported>" }
-  - match: { values.0.12: "<unsupported>" }
-  - match: { values.0.13: "<unsupported>" }
+  - match: { values.0.0: null }
+  - match: { values.0.1: null }
+  - match: { values.0.2: null }
+  - match: { values.0.3: null }
+  - match: { values.0.4: null }
+  - match: { values.0.5: null }
+  - match: { values.0.6: null }
+  - match: { values.0.7: null }
+  - match: { values.0.8: null }
+  - match: { values.0.9: null }
+  - match: { values.0.10: null }
+  - match: { values.0.11: null }
+  - match: { values.0.12: null }
+  - match: { values.0.13: null }
   - match: { values.0.14: "foo bar baz" }
   - match: { values.0.15: Alice }
-  - match: { values.0.16: "<unsupported>" }
-  - match: { values.0.17: "<unsupported>" }
-  - match: { values.0.18: "<unsupported>" }
-  - match: { values.0.19: "<unsupported>" }
-  - match: { values.0.20: "<unsupported>" }
-  - match: { values.0.21: "<unsupported>" }
-  - match: { values.0.22: "<unsupported>" }
+  - match: { values.0.16: null }
+  - match: { values.0.17: null }
+  - match: { values.0.18: null }
+  - match: { values.0.19: null }
+  - match: { values.0.20: null }
+  - match: { values.0.21: null }
+  - match: { values.0.22: null }
   - match: { values.0.23: 12 }
   - match: { values.0.24: xy }
   - match: { values.0.25: "foo bar" }

+ 146 - 0
x-pack/plugin/esql/qa/server/single-node/src/yamlRestTest/resources/rest-api-spec/test/90_non_indexed.yml

@@ -0,0 +1,146 @@
+setup:
+  - skip:
+      features: allowed_warnings_regex
+  - do:
+      indices.create:
+        index: test
+        body:
+          settings:
+            number_of_shards: 5
+          mappings:
+            properties:
+              boolean:
+                type: boolean
+              boolean_noidx:
+                type: boolean
+                index: false
+                doc_values: false
+              date:
+                type: date
+              date_noidx:
+                type: date
+                index: false
+                doc_values: false
+              double:
+                type: double
+              double_noidx:
+                type: double
+                index: false
+                doc_values: false
+              float:
+                type: float
+              float_noidx:
+                type: float
+                index: false
+                doc_values: false
+              integer:
+                type: integer
+              integer_noidx:
+                type: integer
+                index: false
+                doc_values: false
+              ip:
+                type: ip
+              ip_noidx:
+                type: ip
+                index: false
+                doc_values: false
+              keyword:
+                type: keyword
+              keyword_noidx:
+                type: keyword
+                index: false
+                doc_values: false
+              long:
+                type: long
+              long_noidx:
+                type: long
+                index: false
+                doc_values: false
+
+
+  - do:
+      bulk:
+        index: test
+        refresh: true
+        body:
+          - { "index": { } }
+          - {
+            "keyword": "foo",
+            "keyword_noidx": "foo",
+            "boolean": true,
+            "boolean_noidx": true,
+            "integer": 10,
+            "integer_noidx": 10,
+            "long": 20,
+            "long_noidx": 20,
+            "float": 30,
+            "float_noidx": 30,
+            "double": 40,
+            "double_noidx": 40,
+            "date": "2021-04-28T18:50:04.467Z",
+            "date_noidx": "2021-04-28T18:50:04.467Z",
+            "ip": "192.168.0.1",
+            "ip_noidx": "192.168.0.1"
+          }
+
+---
+unsupported:
+  - do:
+      allowed_warnings_regex:
+        - "Field \\[.*\\] cannot be retrieved, it is unsupported or not indexed; returning null"
+      esql.query:
+        body:
+          query: 'from test'
+
+  - match: { columns.0.name: boolean }
+  - match: { columns.0.type: boolean }
+  - match: { columns.1.name: boolean_noidx }
+  - match: { columns.1.type: boolean }
+  - match: { columns.2.name: date }
+  - match: { columns.2.type: date }
+  - match: { columns.3.name: date_noidx }
+  - match: { columns.3.type: date }
+  - match: { columns.4.name: double }
+  - match: { columns.4.type: double }
+  - match: { columns.5.name: double_noidx }
+  - match: { columns.5.type: double }
+  - match: { columns.6.name: float }
+  - match: { columns.6.type: double }
+  - match: { columns.7.name: float_noidx }
+  - match: { columns.7.type: double }
+  - match: { columns.8.name: integer }
+  - match: { columns.8.type: integer }
+  - match: { columns.9.name: integer_noidx }
+  - match: { columns.9.type: integer }
+  - match: { columns.10.name: ip }
+  - match: { columns.10.type: ip }
+  - match: { columns.11.name: ip_noidx }
+  - match: { columns.11.type: ip }
+  - match: { columns.12.name: keyword }
+  - match: { columns.12.type: keyword }
+  - match: { columns.13.name: keyword_noidx }
+  - match: { columns.13.type: keyword }
+  - match: { columns.14.name: long }
+  - match: { columns.14.type: long }
+  - match: { columns.15.name: long_noidx }
+  - match: { columns.15.type: long }
+
+  - length: { values: 1 }
+
+  - match: { values.0.0: true }
+  - match: { values.0.1: null }
+  - match: { values.0.2: "2021-04-28T18:50:04.467Z" }
+  - match: { values.0.3: null }
+  - match: { values.0.4: 40 }
+  - match: { values.0.5: null }
+  - match: { values.0.6: 30 }
+  - match: { values.0.7: null }
+  - match: { values.0.8: 10 }
+  - match: { values.0.9: null }
+  - match: { values.0.10: "192.168.0.1" }
+  - match: { values.0.11: null }
+  - match: { values.0.12: "foo" }
+  - match: { values.0.13: "foo" } # this is a special case, ESQL can retrieve keywords from source
+  - match: { values.0.14: 20 }
+  - match: { values.0.15: null }

+ 5 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-basic.json

@@ -28,6 +28,11 @@
                   "type": "keyword"
                 }
             }
+        },
+        "long_noidx": {
+          "type": "long",
+          "index": false,
+          "doc_values": false
         }
     }
 }

+ 14 - 2
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java

@@ -230,7 +230,7 @@ public class LocalPhysicalPlanOptimizer extends ParameterizedRuleExecutor<Physic
 
         private static boolean isAttributePushable(Expression expression, ScalarFunction operation) {
             if (expression instanceof FieldAttribute f && f.getExactInfo().hasExact()) {
-                return true;
+                return isAggregatable(f);
             }
             if (expression instanceof MetadataAttribute ma && ma.searchable()) {
                 return operation == null
@@ -243,6 +243,17 @@ public class LocalPhysicalPlanOptimizer extends ParameterizedRuleExecutor<Physic
         }
     }
 
+    /**
+     * this method is supposed to be used to define if a field can be used for exact push down (eg. sort or filter).
+     * "aggregatable" is the most accurate information we can have from field_caps as of now.
+     * Pushing down operations on fields that are not aggregatable would result in an error.
+     * @param f
+     * @return
+     */
+    private static boolean isAggregatable(FieldAttribute f) {
+        return f.exactAttribute().field().isAggregatable();
+    }
+
     private static class PushLimitToSource extends OptimizerRule<LimitExec> {
         @Override
         protected PhysicalPlan rule(LimitExec limitExec) {
@@ -280,7 +291,8 @@ public class LocalPhysicalPlanOptimizer extends ParameterizedRuleExecutor<Physic
 
         private boolean canPushDownOrders(List<Order> orders) {
             // allow only exact FieldAttributes (no expressions) for sorting
-            return orders.stream().allMatch(o -> o.child() instanceof FieldAttribute fa && fa.getExactInfo().hasExact());
+            return orders.stream()
+                .allMatch(o -> o.child() instanceof FieldAttribute fa && fa.getExactInfo().hasExact() && isAggregatable(fa));
         }
 
         private List<EsQueryExec.FieldSort> buildFieldSorts(List<Order> orders) {

+ 9 - 8
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java

@@ -213,13 +213,13 @@ public class AnalyzerTests extends ESTestCase {
         assertProjection("""
             from test
             | keep *
-            """, "_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "salary");
+            """, "_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary");
     }
 
     public void testNoProjection() {
         assertProjection("""
             from test
-            """, "_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "salary");
+            """, "_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary");
         assertProjectionTypes(
             """
                 from test
@@ -232,6 +232,7 @@ public class AnalyzerTests extends ESTestCase {
             DataTypes.KEYWORD,
             DataTypes.INTEGER,
             DataTypes.KEYWORD,
+            DataTypes.LONG,
             DataTypes.INTEGER
         );
     }
@@ -240,7 +241,7 @@ public class AnalyzerTests extends ESTestCase {
         assertProjection("""
             from test
             | keep first_name, *, last_name
-            """, "first_name", "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "salary", "last_name");
+            """, "first_name", "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary", "last_name");
     }
 
     public void testProjectThenDropName() {
@@ -272,21 +273,21 @@ public class AnalyzerTests extends ESTestCase {
             from test
             | keep *
             | drop *_name
-            """, "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "salary");
+            """, "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary");
     }
 
     public void testProjectDropNoStarPattern() {
         assertProjection("""
             from test
             | drop *_name
-            """, "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "salary");
+            """, "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary");
     }
 
     public void testProjectOrderPatternWithRest() {
         assertProjection("""
             from test
             | keep *name, *, emp_no
-            """, "first_name", "last_name", "_meta_field", "gender", "job", "job.raw", "languages", "salary", "emp_no");
+            """, "first_name", "last_name", "_meta_field", "gender", "job", "job.raw", "languages", "long_noidx", "salary", "emp_no");
     }
 
     public void testProjectDropPatternAndKeepOthers() {
@@ -423,7 +424,7 @@ public class AnalyzerTests extends ESTestCase {
         assertProjection("""
             from test
             | drop *ala*
-            """, "_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name");
+            """, "_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx");
     }
 
     public void testDropUnsupportedPattern() {
@@ -491,7 +492,7 @@ public class AnalyzerTests extends ESTestCase {
         assertProjection("""
             from test
             | rename emp_no as e, first_name as e
-            """, "_meta_field", "e", "gender", "job", "job.raw", "languages", "last_name", "salary");
+            """, "_meta_field", "e", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary");
     }
 
     public void testRenameUnsupportedField() {

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

@@ -246,7 +246,19 @@ public class LocalLogicalPlanOptimizerTests extends ESTestCase {
         var local = as(localPlan, LocalRelation.class);
         assertThat(
             Expressions.names(local.output()),
-            contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "salary", "x")
+            contains(
+                "_meta_field",
+                "emp_no",
+                "first_name",
+                "gender",
+                "job",
+                "job.raw",
+                "languages",
+                "last_name",
+                "long_noidx",
+                "salary",
+                "x"
+            )
         );
     }
 

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

@@ -390,7 +390,7 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
         var extract = as(project.child(), FieldExtractExec.class);
         assertThat(
             names(extract.attributesToExtract()),
-            contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "salary")
+            contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary")
         );
     }
 
@@ -420,7 +420,7 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
         var extract = as(project.child(), FieldExtractExec.class);
         assertThat(
             names(extract.attributesToExtract()),
-            contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "salary")
+            contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary")
         );
     }
 
@@ -877,7 +877,7 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
 
         assertThat(
             names(extract.attributesToExtract()),
-            contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "salary")
+            contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary")
         );
 
         var source = source(extract.child());
@@ -1683,6 +1683,24 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
         assertNull(source.query());
     }
 
+    public void testNoNonIndexedFilterPushDown() {
+        var plan = physicalPlan("""
+            from test
+            | where long_noidx == 1
+            """);
+
+        var optimized = optimizedPlan(plan);
+        var limit = as(optimized, LimitExec.class);
+        var exchange = asRemoteExchange(limit.child());
+        var project = as(exchange.child(), ProjectExec.class);
+        var extract = as(project.child(), FieldExtractExec.class);
+        var limit2 = as(extract.child(), LimitExec.class);
+        var filter = as(limit2.child(), FilterExec.class);
+        var extract2 = as(filter.child(), FieldExtractExec.class);
+        var source = source(extract2.child());
+        assertNull(source.query());
+    }
+
     public void testTextWithRawFilterPushDown() {
         var plan = physicalPlan("""
             from test
@@ -1716,6 +1734,23 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
         assertNull(source.sorts());
     }
 
+    public void testNoNonIndexedSortPushDown() {
+        var plan = physicalPlan("""
+            from test
+            | sort long_noidx
+            """);
+
+        var optimized = optimizedPlan(plan);
+        var topN = as(optimized, TopNExec.class);
+        var exchange = as(topN.child(), ExchangeExec.class);
+        var project = as(exchange.child(), ProjectExec.class);
+        var extract = as(project.child(), FieldExtractExec.class);
+        var topN2 = as(extract.child(), TopNExec.class);
+        var extract2 = as(topN2.child(), FieldExtractExec.class);
+        var source = source(extract2.child());
+        assertNull(source.sorts());
+    }
+
     public void testTextWithRawSortPushDown() {
         var plan = physicalPlan("""
             from test