This is an automated email from the ASF dual-hosted git repository.

amashenkov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 1bc2dfb6895 IGNITE-28389 SQL. Eliminate table scans with always_false 
predicate (#7959)
1bc2dfb6895 is described below

commit 1bc2dfb6895449054c236dfe105392d9ae6750f7
Author: amashenkov <[email protected]>
AuthorDate: Wed Apr 15 12:38:50 2026 +0300

    IGNITE-28389 SQL. Eliminate table scans with always_false predicate (#7959)
---
 .../ignite/internal/sql/engine/ItDmlTest.java      | 12 ++++
 .../internal/sql/engine/prepare/PlannerHelper.java |  2 +
 .../internal/sql/engine/prepare/PlannerPhase.java  | 25 ++++++++
 .../engine/rule/logical/FilterScanMergeRule.java   | 16 +++++
 .../engine/rule/logical/PruneTableModifyRule.java  | 74 ++++++++++++++++++++++
 .../planner/ProjectFilterScanMergePlannerTest.java | 72 +++++++++++++++++++++
 .../resources/mapping/test_partition_pruning.test  | 53 ----------------
 7 files changed, 201 insertions(+), 53 deletions(-)

diff --git 
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDmlTest.java
 
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDmlTest.java
index f2011008192..988249c5b67 100644
--- 
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDmlTest.java
+++ 
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDmlTest.java
@@ -55,6 +55,7 @@ import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.tx.Transaction;
 import org.apache.ignite.tx.TransactionOptions;
+import org.hamcrest.Matchers;
 import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Disabled;
@@ -1179,6 +1180,17 @@ public class ItDmlTest extends BaseSqlIntegrationTest {
         );
     }
 
+    @Test
+    public void insertFromSelectWithAlwaysFalseCondition() {
+        sql("CREATE TABLE test (id INT PRIMARY KEY, val REAL)");
+        sql("CREATE TABLE test2 (id INT PRIMARY KEY, val REAL)");
+
+        assertQuery("INSERT INTO test2 SELECT id, val FROM test WHERE val > 1 
AND val < 0")
+                .matches(Matchers.not(containsSubPlan("TableModify")))
+                .returns(0L)
+                .check();
+    }
+
     private static Stream<Arguments> decimalLimits() {
         return Stream.of(
                 arguments(SqlTypeName.BIGINT.getName(), Long.MAX_VALUE, 
Long.MIN_VALUE),
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerHelper.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerHelper.java
index 89f9134fec1..1d565ccd4dd 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerHelper.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerHelper.java
@@ -182,6 +182,8 @@ public final class PlannerHelper {
 
             rel = planner.transform(PlannerPhase.HEP_PROJECT_PUSH_DOWN, 
rel.getTraitSet(), rel);
 
+            rel = planner.transform(PlannerPhase.HEP_EMPTY_NODES_ELIMINATION, 
rel.getTraitSet(), rel);
+
             if (fastQueryOptimizationEnabled()) {
                 // the sole purpose of this code block is to limit scope of 
`simpleOperation` variable.
                 // The result of `HEP_TO_SIMPLE_KEY_VALUE_OPERATION` phase 
MUST NOT be passed to next stage,
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerPhase.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerPhase.java
index 303f08400a4..9631e6d28b8 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerPhase.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerPhase.java
@@ -78,6 +78,7 @@ import 
org.apache.ignite.internal.sql.engine.rule.logical.IgniteMultiJoinOptimiz
 import 
org.apache.ignite.internal.sql.engine.rule.logical.IgniteProjectCorrelateTransposeRule;
 import org.apache.ignite.internal.sql.engine.rule.logical.LogicalOrToUnionRule;
 import org.apache.ignite.internal.sql.engine.rule.logical.ProjectScanMergeRule;
+import org.apache.ignite.internal.sql.engine.rule.logical.PruneTableModifyRule;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 
 /**
@@ -161,6 +162,22 @@ public enum PlannerPhase {
         }
     },
 
+    HEP_EMPTY_NODES_ELIMINATION(
+            "Heuristic phase to eliminate empty nodes",
+            PruneEmptyRules.PROJECT_INSTANCE,
+            PruneEmptyRules.FILTER_INSTANCE,
+            PruneEmptyRules.SORT_INSTANCE,
+            PruneEmptyRules.AGGREGATE_INSTANCE,
+            PruneEmptyRules.JOIN_LEFT_INSTANCE,
+            PruneEmptyRules.JOIN_RIGHT_INSTANCE
+    ) {
+        /** {@inheritDoc} */
+        @Override
+        public Program getProgram(PlanningContext ctx) {
+            return hep(getRules(ctx));
+        }
+    },
+
     HEP_OPTIMIZE_JOIN_ORDER(
             "Heuristic phase to optimize join order"
     ) {
@@ -207,6 +224,13 @@ public enum PlannerPhase {
             IgniteJoinConditionPushRule.INSTANCE,
             CoreRules.JOIN_PUSH_TRANSITIVE_PREDICATES,
 
+            PruneEmptyRules.PROJECT_INSTANCE,
+            PruneEmptyRules.FILTER_INSTANCE,
+            PruneEmptyRules.SORT_INSTANCE,
+            PruneEmptyRules.AGGREGATE_INSTANCE,
+            PruneEmptyRules.JOIN_LEFT_INSTANCE,
+            PruneEmptyRules.JOIN_RIGHT_INSTANCE,
+
             FilterIntoJoinRule.FilterIntoJoinRuleConfig.DEFAULT
                     .withOperandSupplier(b0 ->
                             b0.operand(LogicalFilter.class).oneInput(b1 ->
@@ -255,6 +279,7 @@ public enum PlannerPhase {
 
             PruneEmptyRules.CORRELATE_LEFT_INSTANCE,
             PruneEmptyRules.CORRELATE_RIGHT_INSTANCE,
+            PruneTableModifyRule.INSTANCE,
 
             // Useful of this rule is not clear now.
             // CoreRules.AGGREGATE_REDUCE_FUNCTIONS,
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/FilterScanMergeRule.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/FilterScanMergeRule.java
index 77828bb5de7..448c8d77a35 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/FilterScanMergeRule.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/FilterScanMergeRule.java
@@ -26,6 +26,7 @@ import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rel.logical.LogicalValues;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexNode;
@@ -99,6 +100,21 @@ public abstract class FilterScanMergeRule<T extends 
ProjectableFilterableTableSc
         // We need to replace RexInputRef with RexLocalRef because TableScan 
doesn't have inputs.
         condition = RexUtils.replaceInputRefs(condition);
 
+        // Eliminate scan if always false condition found.
+        if (condition.isAlwaysFalse()) {
+            call.transformTo(LogicalValues.createEmpty(cluster, 
scan.getRowType()));
+            call.getPlanner().prune(filter);
+            call.getPlanner().prune(scan);
+            return;
+        }
+
+        // Eliminate always true condition.
+        if (condition.isAlwaysTrue()) {
+            call.transformTo(scan);
+            call.getPlanner().prune(filter);
+            return;
+        }
+
         // Set default traits, real traits will be calculated for physical 
node.
         RelTraitSet trait = cluster.traitSet();
 
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/PruneTableModifyRule.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/PruneTableModifyRule.java
new file mode 100644
index 00000000000..fe8164af5d3
--- /dev/null
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/PruneTableModifyRule.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.rule.logical;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.TableModify;
+import org.apache.calcite.rel.core.Values;
+import org.apache.calcite.rel.rules.SubstitutionRule;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.ignite.internal.sql.engine.rex.IgniteRexBuilder;
+import 
org.apache.ignite.internal.sql.engine.rule.logical.PruneTableModifyRule.Config;
+import org.immutables.value.Value;
+
+/**
+ * Rule that eliminates table modify node if it doesn't have any source rows.
+ */
[email protected]
+public class PruneTableModifyRule extends RelRule<Config> implements 
SubstitutionRule {
+    public static final RelOptRule INSTANCE = Config.DEFAULT.toRule();
+
+    /**
+     * Constructor.
+     *
+     * @param config Rule configuration.
+     */
+    private PruneTableModifyRule(PruneTableModifyRule.Config config) {
+        super(config);
+    }
+
+    @Override public void onMatch(RelOptRuleCall call) {
+        TableModify singleRel = call.rel(0);
+
+        // TODO https://issues.apache.org/jira/browse/IGNITE-23512: Default 
Calcite RexBuilder ignores field type and extract type from 
+        //  the given value. E.g. for zero value RexBuilder creates INT 
literal. Use simple way create `singleValue` after fixing the issue.
+        // RelNode singleValue = call.builder().values(singleRel.getRowType(), 
0L).build();
+        RexLiteral zeroLiteral = IgniteRexBuilder.INSTANCE.makeLiteral(0L, 
singleRel.getRowType().getFieldList().get(0).getType());
+        RelNode singleValue = 
call.builder().values(List.of(List.of(zeroLiteral)), 
singleRel.getRowType()).build();
+
+        singleValue = singleValue.copy(singleRel.getCluster().traitSet(), 
Collections.emptyList());
+        call.transformTo(singleValue);
+    }
+
+    /** Rule configuration. */
+    @Value.Immutable(singleton = false)
+    public interface Config extends RuleFactoryConfig<Config> {
+        Config DEFAULT = ImmutablePruneTableModifyRule.Config.builder()
+                .withDescription("PruneTableModify")
+                .withRuleFactory(PruneTableModifyRule::new)
+                .withOperandSupplier(b0 ->
+                        b0.operand(TableModify.class).oneInput(b1 ->
+                        
b1.operand(Values.class).predicate(Values::isEmpty).noInputs()))
+                .build();
+    }
+}
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ProjectFilterScanMergePlannerTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ProjectFilterScanMergePlannerTest.java
index 71c015daf1d..1ae5e6faad5 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ProjectFilterScanMergePlannerTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ProjectFilterScanMergePlannerTest.java
@@ -19,8 +19,10 @@ package org.apache.ignite.internal.sql.engine.planner;
 
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Predicate;
 import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
+import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableIntList;
 import 
org.apache.ignite.internal.sql.engine.framework.TestBuilders.TableBuilder;
@@ -28,6 +30,7 @@ import 
org.apache.ignite.internal.sql.engine.prepare.bounds.SearchBounds;
 import org.apache.ignite.internal.sql.engine.rel.IgniteAggregate;
 import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTableScan;
+import org.apache.ignite.internal.sql.engine.rel.IgniteValues;
 import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
 import org.apache.ignite.internal.type.NativeTypes;
@@ -275,6 +278,75 @@ public class ProjectFilterScanMergePlannerTest extends 
AbstractPlannerTest {
                 "ProjectFilterTransposeRule", "FilterProjectTransposeRule");
     }
 
+    @Test
+    public void testAlwaysTrueFilterPruning() throws Exception {
+        String sql = "SELECT a, c FROM tbl WHERE a > 1 OR a < 3 OR a IS NULL";
+
+        assertPlan(sql, publicSchema, isInstanceOf(IgniteTableScan.class)
+                        .and(scan -> scan.projects() == null)
+                        .and(scan -> scan.condition() == null)
+                        .and(scan -> ImmutableIntList.of(0, 
2).equals(scan.requiredColumns())),
+                "ProjectFilterTransposeRule", "FilterProjectTransposeRule");
+    }
+
+    @Test
+    public void testAlwaysFalseFilterPruning() throws Exception {
+        Predicate<IgniteValues> hasEmptyValuesOnly = 
hasEmptyValuesOnlyPredicate();
+
+        // Table scan elimination.
+        String sql = "SELECT a, c FROM tbl WHERE a > 1 AND a < 0";
+        assertPlan(sql, publicSchema, hasEmptyValuesOnly);
+
+        sql = "SELECT a, c FROM (SELECT a, c FROM tbl WHERE a > 1) WHERE c = 1 
AND c IS NULL";
+        assertPlan(sql, publicSchema, hasEmptyValuesOnly,
+                "ProjectFilterTransposeRule", "FilterProjectTransposeRule");
+
+        sql = "SELECT a, c FROM (SELECT a, c FROM tbl WHERE a > 1) WHERE a < 
0";
+        assertPlan(sql, publicSchema, hasEmptyValuesOnly,
+                "ProjectFilterTransposeRule", "FilterProjectTransposeRule");
+
+        // JOIN branch elimination.
+        sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 LEFT JOIN tbl AS t2 ON 
t1.a = t2.a WHERE t2.a = 1 AND t2.a IS NULL AND t1.c = 1";
+        assertPlan(sql, publicSchema, hasEmptyValuesOnly);
+
+        sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 INNER JOIN tbl AS t2 ON 
t1.a = t2.a WHERE t2.a = 1 AND t2.a IS NULL";
+        assertPlan(sql, publicSchema, hasEmptyValuesOnly);
+
+        sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 INNER JOIN tbl AS t2 ON 
t1.a = t2.a WHERE t1.a = 1 AND t2.a = 2";
+        assertPlan(sql, publicSchema, hasEmptyValuesOnlyPredicate());
+    }
+
+    @Test
+    public void testJoinWithAlwaysFalseConditionPruning() throws Exception {
+        String sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 LEFT JOIN tbl AS 
t2 ON (t1.a = t2.a AND t2.a = 1 AND t2.a = 2) WHERE t1.c = 1";
+        assertPlan(sql, publicSchema, isInstanceOf(IgniteTableScan.class)
+                .and(scan -> scan.projects() != null)
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=($t1, 1)".equals(scan.condition().toString()))
+        );
+
+        sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 INNER JOIN tbl AS t2 ON 
t1.a = t2.a AND t2.a = 1 AND t2.a = 2";
+        assertPlan(sql, publicSchema, hasEmptyValuesOnlyPredicate());
+    }
+
+    @Test
+    public void testAlwaysFalseFilterPruningWithDml() throws Exception {
+        Predicate<IgniteValues> zeroDmlResultPredicate = 
isInstanceOf(IgniteValues.class)
+                .and(values -> values.getTuples().size() == 1) // single row
+                .and(values -> values.getTuples().get(0).size() == 1) // row 
of single column
+                .and(values -> 
RexLiteral.longValue(values.getTuples().get(0).get(0)) == 0L);
+
+        String sql = "INSERT INTO tbl (a, c) SELECT a, b FROM tbl WHERE a > 1 
AND a < 0";
+        assertPlan(sql, publicSchema, zeroDmlResultPredicate);
+
+        sql = "INSERT INTO tbl (a, c) (SELECT a, c FROM (SELECT a, c FROM tbl 
WHERE a > 1) WHERE a < 0)";
+        assertPlan(sql, publicSchema, zeroDmlResultPredicate);
+    }
+
+    private Predicate<IgniteValues> hasEmptyValuesOnlyPredicate() {
+        return isInstanceOf(IgniteValues.class).and(values -> 
values.getTuples().isEmpty());
+    }
+
     /**
      * Convert search bounds to RexNodes.
      */
diff --git 
a/modules/sql-engine/src/test/resources/mapping/test_partition_pruning.test 
b/modules/sql-engine/src/test/resources/mapping/test_partition_pruning.test
index d0932226f45..febc16e2780 100644
--- a/modules/sql-engine/src/test/resources/mapping/test_partition_pruning.test
+++ b/modules/sql-engine/src/test/resources/mapping/test_partition_pruning.test
@@ -163,59 +163,6 @@ Fragment#4
             fieldNames: [ID, C1, C2]
             est: (rows=1)
 ---
-# Self join, different predicates that produce disjoint set of partitions
-# TODO https://issues.apache.org/jira/browse/IGNITE-28389: Fix the test. We 
expect the mapper should eliminate all the disjoined parts.
-N1
-SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'HashJoinConverter', 
'CorrelatedNestedLoopJoin') */ *
-  FROM t1_n1n2n3 as t1, t1_n1n2n3 as t2
- WHERE t1.id = t2.id and t1.id IN (1, 3) and t2.id IN (42, 44)
----
-Fragment#2 root
-  distribution: single
-  executionNodes: [N1]
-  exchangeSourceNodes: {3=[N1, N2, N3]}
-  colocationGroup[-1]: {nodes=[N1], sourceIds=[-1, 3], assignments={}, 
partitionsWithConsistencyTokens={N1=[]}}
-  colocationGroup[3]: {nodes=[N1], sourceIds=[-1, 3], assignments={}, 
partitionsWithConsistencyTokens={N1=[]}}
-  tree: 
-    Receiver
-        fieldNames: [ID, C1, C2, ID$0, C1$0, C2$0]
-        sourceFragmentId: 3
-        est: (rows=1)
-
-Fragment#3
-  distribution: table PUBLIC.T1_N1N2N3 in zone ZONE_1
-  executionNodes: [N1, N2, N3]
-  targetNodes: [N1]
-  colocationGroup[0]: {nodes=[N1, N2, N3], sourceIds=[0, 1], 
assignments={part_0=N1:3, part_1=N2:3, part_2=N3:3}, 
partitionsWithConsistencyTokens={N1=[part_0:3], N2=[part_1:3], N3=[part_2:3]}}
-  colocationGroup[1]: {nodes=[N1, N2, N3], sourceIds=[0, 1], 
assignments={part_0=N1:3, part_1=N2:3, part_2=N3:3}, 
partitionsWithConsistencyTokens={N1=[part_0:3], N2=[part_1:3], N3=[part_2:3]}}
-  partitions: [T1_N1N2N3=[N1={0}, N2={1}, N3={2}]]
-  tree: 
-    Sender
-        distribution: single
-        targetFragmentId: 2
-        est: (rows=6250)
-      MergeJoin
-          predicate: =(ID, ID$0)
-          fieldNames: [ID, C1, C2, ID$0, C1$0, C2$0]
-          type: inner
-          est: (rows=6250)
-        Sort
-            collation: [ID ASC]
-            est: (rows=25000)
-          TableScan
-              table: PUBLIC.T1_N1N2N3
-              predicate: false
-              fieldNames: [ID, C1, C2]
-              est: (rows=25000)
-        Sort
-            collation: [ID ASC]
-            est: (rows=25000)
-          TableScan
-              table: PUBLIC.T1_N1N2N3
-              predicate: false
-              fieldNames: [ID, C1, C2]
-              est: (rows=25000)
----
 # Correlated
 # Prune partitions from left arm statically, and pass meta to the right arm.
 # Same set of nodes.

Reply via email to