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

morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new df36be5a86d [fix](point query) Keep point-query scan when partition 
pruning is empty (#67161)
df36be5a86d is described below

commit df36be5a86d916cb8b94b739482f744a0e129f1b
Author: zyp-V <[email protected]>
AuthorDate: Tue Sep 1 10:27:13 2026 +0800

    [fix](point query) Keep point-query scan when partition pruning is empty 
(#67161)
    
    ### What problem does this PR solve?
    
    Problem Summary: Short-circuit point queries on partitioned tables were
    rewritten to LogicalEmptyRelation when their partition predicates
    matched no existing partition. This removed the LogicalOlapScan required
    to initialize and cache ShortCircuitQueryContext, causing the
    point-query path to fail or fall back to normal planning. Preserve a
    partition-pruned scan with an empty selected partition list only for
    short-circuit point queries, while retaining LogicalEmptyRelation for
    ordinary queries and preserving the effective partition-predicate
    marker.
    
    ### Release note
    
    Keep short-circuit point-query planning active when no table partition
    matches the query predicate.
    
    
    Co-authored-by: zhangyipeng.0818 <[email protected]>
---
 .../java/org/apache/doris/catalog/OlapTable.java   |  21 +++-
 .../LogicalResultSinkToShortCircuitPointQuery.java |  16 ++-
 .../rules/rewrite/PruneOlapScanPartition.java      |  12 +-
 .../trees/plans/commands/ExecuteCommand.java       |  15 ++-
 .../apache/doris/qe/ShortCircuitQueryContext.java  |   6 +-
 .../java/org/apache/doris/qe/StmtExecutor.java     |  10 +-
 .../org/apache/doris/catalog/OlapTableTest.java    |  29 +++++
 .../rules/rewrite/ShortCircuitPointQueryTest.java  | 118 +++++++++++++++++--
 .../doris/qe/ShortCircuitQueryContextTest.java     |  29 +++++
 .../test_point_query_partition_not_exists.out      |  21 ++++
 .../test_point_query_partition_not_exists.groovy   | 130 +++++++++++++++++++++
 11 files changed, 381 insertions(+), 26 deletions(-)

diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
index eec6e523ad1..a3eef1688b4 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
@@ -125,6 +125,7 @@ import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.stream.Collectors;
@@ -187,6 +188,10 @@ public class OlapTable extends Table implements 
MTMVRelatedTableIf, GsonPostProc
     @SerializedName(value = "itp", alternate = {"idToPartition"})
     @Getter
     protected ConcurrentHashMap<Long, Partition> idToPartition = new 
ConcurrentHashMap<>();
+    // Incremented only when the formal partition id set changes. Prepared 
short-circuit point query cache uses this
+    // to invalidate stale partition pruning metadata without reacting to 
ordinary data writes. This is transient and
+    // rebuilt from zero after deserialization because prepared statement 
cache is also in-memory.
+    private transient AtomicLong partitionTopologyVersion = new AtomicLong(0L);
     // handled in postgsonprocess
     @Getter
     protected Map<String, Partition> nameToPartition = Maps.newTreeMap();
@@ -1335,8 +1340,11 @@ public class OlapTable extends Table implements 
MTMVRelatedTableIf, GsonPostProc
     }
 
     public void addPartition(Partition partition) {
-        idToPartition.put(partition.getId(), partition);
+        Partition previousPartition = idToPartition.put(partition.getId(), 
partition);
         nameToPartition.put(partition.getName(), partition);
+        if (previousPartition == null) {
+            bumpPartitionTopologyVersion();
+        }
     }
 
     // This is a private method.
@@ -1352,6 +1360,7 @@ public class OlapTable extends Table implements 
MTMVRelatedTableIf, GsonPostProc
         if (partition != null) {
             idToPartition.remove(partition.getId());
             nameToPartition.remove(partitionName);
+            bumpPartitionTopologyVersion();
             RecyclePartitionParam recyclePartitionParam = new 
RecyclePartitionParam();
             fillInfo(partition, recyclePartitionParam);
             dropPartitionCommon(dbId, isForceDrop, recyclePartitionParam, 
partition, reserveTablets);
@@ -1649,6 +1658,14 @@ public class OlapTable extends Table implements 
MTMVRelatedTableIf, GsonPostProc
         return new ArrayList<>(idToPartition.keySet());
     }
 
+    public long getPartitionTopologyVersion() {
+        return partitionTopologyVersion.get();
+    }
+
+    private void bumpPartitionTopologyVersion() {
+        partitionTopologyVersion.incrementAndGet();
+    }
+
     public Set<String> getCopiedBfColumns() {
         if (bfColumns == null) {
             return null;
@@ -2079,6 +2096,7 @@ public class OlapTable extends Table implements 
MTMVRelatedTableIf, GsonPostProc
 
     @Override
     public void gsonPostProcess() throws IOException {
+        partitionTopologyVersion = new AtomicLong(0L);
 
         // HACK: the index id in MaterializedIndexMeta is not equals to the 
index id
         // saved in OlapTable, because the table restore from snapshot is not 
reset
@@ -2239,6 +2257,7 @@ public class OlapTable extends Table implements 
MTMVRelatedTableIf, GsonPostProc
 
         idToPartition.put(newPartition.getId(), newPartition);
         nameToPartition.put(newPartition.getName(), newPartition);
+        bumpPartitionTopologyVersion();
 
         DataProperty dataProperty = 
partitionInfo.getDataProperty(oldPartition.getId());
         ReplicaAllocation replicaAlloc = 
partitionInfo.getReplicaAllocation(oldPartition.getId());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
index dfcd2ea289c..f839b81b601 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
@@ -19,6 +19,7 @@ package org.apache.doris.nereids.rules.rewrite;
 
 import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.datasource.doris.RemoteOlapTable;
 import org.apache.doris.nereids.StatementContext;
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
@@ -31,6 +32,7 @@ import 
org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.qe.ConnectContext;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 
@@ -60,11 +62,23 @@ public class LogicalResultSinkToShortCircuitPointQuery 
implements RewriteRuleFac
                         && expression.child(1).isLiteral());
     }
 
-    private boolean scanMatchShortCircuitCondition(LogicalOlapScan olapScan) {
+    @VisibleForTesting
+    boolean scanMatchShortCircuitCondition(LogicalOlapScan olapScan) {
         if 
(!ConnectContext.get().getSessionVariable().isEnableShortCircuitQuery()) {
             return false;
         }
+        // Lazy point-query pruning does not preserve explicit 
PARTITION/TABLET restrictions.
+        // Keep these queries on the normal execution path so the physical 
scan enforces them.
+        if (!olapScan.getManuallySpecifiedPartitions().isEmpty()
+                || !olapScan.getManuallySpecifiedTabletIds().isEmpty()) {
+            return false;
+        }
         OlapTable olapTable = olapScan.getTable();
+        // Remote Doris metadata refresh replaces the RemoteOlapTable 
instance. A prepared context retains the old
+        // instance, so its table-local topology version cannot observe remote 
partition changes.
+        if (olapTable instanceof RemoteOlapTable) {
+            return false;
+        }
         if (olapTable.hasVariantColumns()) {
             return false;
         }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneOlapScanPartition.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneOlapScanPartition.java
index aa5f722587f..c89234b92fe 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneOlapScanPartition.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneOlapScanPartition.java
@@ -132,14 +132,20 @@ public class PruneOlapScanPartition implements 
RewriteRuleFactory {
         if (prunedPartitions == null) {
             return Pair.of(null, Optional.empty());
         }
+        boolean hasPartitionPredicate = 
prunedPartitionsByFilters.hasPartitionPredicate
+                || !scan.getManuallySpecifiedPartitions().isEmpty()
+                || !scan.getManuallySpecifiedTabletIds().isEmpty();
         if (prunedPartitions.isEmpty()) {
+            // Keep scan for short-circuit point queries even when no 
partition matches.
+            // ShortCircuitQueryContext needs the scan to initialize and cache 
the point-query path.
+            if (filter != null && ctx.statementContext.isShortCircuitQuery()) {
+                return Pair.of(scan.withSelectedPartitionIds(prunedPartitions, 
hasPartitionPredicate),
+                        Optional.empty());
+            }
             return Pair.of(new LogicalEmptyRelation(
                 ConnectContext.get().getStatementContext().getNextRelationId(),
                 ctx.root.getOutput()), Optional.empty());
         }
-        boolean hasPartitionPredicate = 
prunedPartitionsByFilters.hasPartitionPredicate
-                || !scan.getManuallySpecifiedPartitions().isEmpty()
-                || !scan.getManuallySpecifiedTabletIds().isEmpty();
         return Pair.of(scan.withSelectedPartitionIds(prunedPartitions,
                 hasPartitionPredicate),
                 prunedPartitionsByFilters.prunedPartitionPredicate);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
index 21f6c4e5063..dd7b551585c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
@@ -17,7 +17,6 @@
 
 package org.apache.doris.nereids.trees.plans.commands;
 
-import org.apache.doris.analysis.Queriable;
 import org.apache.doris.analysis.StatementBase;
 import org.apache.doris.analysis.StmtType;
 import org.apache.doris.analysis.TableScanParams;
@@ -46,6 +45,7 @@ import org.apache.doris.qe.PreparedStatementContext;
 import org.apache.doris.qe.ShortCircuitQueryContext;
 import org.apache.doris.qe.StmtExecutor;
 
+import com.google.common.base.Preconditions;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -181,11 +181,14 @@ public class ExecuteCommand extends Command {
         // early above, has just been refreshed here, or is stale and we are 
about to re-plan.
         preparedStmtCtx.shortCircuitQueryContext = Optional.empty();
         executor.execute();
-        if (executor.getContext().getStatementContext().isShortCircuitQuery()) 
{
-            // cache short-circuit plan
-            preparedStmtCtx.shortCircuitQueryContext = Optional.of(
-                    new ShortCircuitQueryContext(executor.planner(), 
(Queriable) executor.getParsedStmt()));
-            
statementContext.setShortCircuitQueryContext(preparedStmtCtx.shortCircuitQueryContext.get());
+        StatementContext executedStatementContext = 
executor.getContext().getStatementContext();
+        ShortCircuitQueryContext shortCircuitQueryContext =
+                executedStatementContext.getShortCircuitQueryContext();
+        if (shortCircuitQueryContext != null) {
+            
Preconditions.checkState(executedStatementContext.isShortCircuitQuery());
+            // Publish the exact context used by this execution so its 
topology generation stays
+            // bound to the cached partition pruner in the same planner scan 
node.
+            preparedStmtCtx.shortCircuitQueryContext = 
Optional.of(shortCircuitQueryContext);
         }
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java 
b/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
index 2a4235e332d..99496be25c9 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
@@ -60,6 +60,7 @@ public class ShortCircuitQueryContext {
     public final OlapTable tbl;
     public final String tableName;
     private final long fileCacheQueryLimitBytes;
+    private final long partitionTopologyVersion;
 
     public final OlapScanNode scanNode;
     public final Queriable analzyedQuery;
@@ -115,6 +116,7 @@ public class ShortCircuitQueryContext {
         this.tbl = this.scanNode.getOlapTable();
         this.tableName = this.scanNode.getTableNameInPlan();
         this.schemaVersion = this.tbl.getBaseSchemaVersion();
+        this.partitionTopologyVersion = this.tbl.getPartitionTopologyVersion();
         this.analzyedQuery = analzyedQuery;
     }
 
@@ -130,6 +132,7 @@ public class ShortCircuitQueryContext {
         this.tableName = tableName;
         this.schemaVersion = schemaVersion;
         this.fileCacheQueryLimitBytes = fileCacheQueryLimitBytes;
+        this.partitionTopologyVersion = tbl.getPartitionTopologyVersion();
         this.scanNode = null;
         this.analzyedQuery = null;
     }
@@ -138,7 +141,8 @@ public class ShortCircuitQueryContext {
         return !this.tbl.isDropped
                 && this.tbl.getBaseSchemaVersion() == this.schemaVersion
                 && Objects.equals(this.tableName, this.tbl.getName())
-                && this.fileCacheQueryLimitBytes == 
ctx.getSessionVariable().fileCacheQueryLimitBytes;
+                && this.fileCacheQueryLimitBytes == 
ctx.getSessionVariable().fileCacheQueryLimitBytes
+                && this.tbl.getPartitionTopologyVersion() == 
this.partitionTopologyVersion;
     }
 
     public void sanitize() {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java 
b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
index 76456330951..797140bd408 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
@@ -1491,10 +1491,12 @@ public class StmtExecutor {
         RowBatch batch;
         CoordInterface coordBase = null;
         if (statementContext.isShortCircuitQuery()) {
-            ShortCircuitQueryContext shortCircuitQueryContext =
-                    statementContext.getShortCircuitQueryContext() != null
-                            ? statementContext.getShortCircuitQueryContext()
-                            : new ShortCircuitQueryContext(planner, 
(Queriable) parsedStmt);
+            ShortCircuitQueryContext shortCircuitQueryContext = 
statementContext.getShortCircuitQueryContext();
+            if (shortCircuitQueryContext == null) {
+                shortCircuitQueryContext = new 
ShortCircuitQueryContext(planner, (Queriable) parsedStmt);
+                // ExecuteCommand publishes this same context after a 
successful first prepared execution.
+                
statementContext.setShortCircuitQueryContext(shortCircuitQueryContext);
+            }
             coordBase = new PointQueryExecutor(shortCircuitQueryContext,
                     
context.getSessionVariable().getMaxMsgSizeOfResultReceiver());
             context.getState().setIsQuery(true);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableTest.java
index 4503cb58067..81090d8d7dd 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableTest.java
@@ -76,6 +76,35 @@ public class OlapTableTest {
         Assert.assertEquals(132L, stats.getIndexLength());
     }
 
+    @Test
+    public void testPartitionTopologyVersionChangesWithPartitionIdSet() {
+        OlapTable olapTable = new OlapTable();
+        olapTable.setPartitionInfo(new SinglePartitionInfo());
+
+        long version = olapTable.getPartitionTopologyVersion();
+        addPartitionForTopologyVersionTest(olapTable, 1L, "p1");
+        Assert.assertEquals(version + 1, 
olapTable.getPartitionTopologyVersion());
+
+        version = olapTable.getPartitionTopologyVersion();
+        olapTable.replacePartition(newPartitionForTopologyVersionTest(2L, 
"p1"), new RecyclePartitionParam());
+        Assert.assertEquals(version + 1, 
olapTable.getPartitionTopologyVersion());
+
+        version = olapTable.getPartitionTopologyVersion();
+        olapTable.dropPartitionAndReserveTablet("p1");
+        Assert.assertEquals(version + 1, 
olapTable.getPartitionTopologyVersion());
+    }
+
+    private void addPartitionForTopologyVersionTest(OlapTable olapTable, long 
partitionId, String partitionName) {
+        olapTable.getPartitionInfo().addPartition(partitionId, new 
DataProperty(TStorageMedium.HDD),
+                new ReplicaAllocation((short) 1), false, true);
+        olapTable.addPartition(newPartitionForTopologyVersionTest(partitionId, 
partitionName));
+    }
+
+    private Partition newPartitionForTopologyVersionTest(long partitionId, 
String partitionName) {
+        MaterializedIndex index = new MaterializedIndex(partitionId, 
MaterializedIndex.IndexState.NORMAL);
+        return new Partition(partitionId, partitionName, index, new 
RandomDistributionInfo(1));
+    }
+
     private Replica mockReplica(Replica.ReplicaState state, long dataSize, 
long localSegmentSize,
             long remoteSegmentSize, long localIndexSize, long remoteIndexSize) 
{
         Replica replica = Mockito.mock(Replica.class);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
index e036b3a7c49..1b25c4728d7 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
@@ -17,7 +17,13 @@
 
 package org.apache.doris.nereids.rules.rewrite;
 
+import org.apache.doris.catalog.Database;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.Tablet;
 import org.apache.doris.common.FeConstants;
+import org.apache.doris.datasource.doris.RemoteOlapTable;
+import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
@@ -31,8 +37,9 @@ import org.junit.jupiter.api.Test;
 /**
  * Regression test:
  * For short-circuit point query, we should not rewrite LogicalOlapScan to 
LogicalEmptyRelation
- * even if the table partitions are empty. Otherwise PreparedStatement could 
not cache
- * ShortCircuitQueryContext and may downgrade to normal planning repeatedly.
+ * even if the table partitions are empty or partition pruning selects no 
partitions.
+ * Current execution still needs the scan to initialize the point-query path; 
PreparedStatement
+ * cacheability for empty selected partitions is handled separately.
  */
 class ShortCircuitPointQueryTest extends TestWithFeService
         implements MemoPatternMatchSupported {
@@ -53,24 +60,115 @@ class ShortCircuitPointQueryTest extends TestWithFeService
                 + "  \"light_schema_change\" = \"true\",\n"
                 + "  \"store_row_column\" = \"true\"\n"
                 + ");");
+        createTable("CREATE TABLE `tbl_partitioned_point_query` (\n"
+                + "  `order_id` bigint NOT NULL,\n"
+                + "  `pay_date` date NOT NULL,\n"
+                + "  `v1` varchar(30) NULL\n"
+                + ") ENGINE=OLAP\n"
+                + "UNIQUE KEY(`order_id`, `pay_date`)\n"
+                + "PARTITION BY RANGE(`pay_date`) (\n"
+                + "  PARTITION `p20260805` VALUES [(\"2026-08-05\"), 
(\"2026-08-06\"))\n"
+                + ")\n"
+                + "DISTRIBUTED BY HASH(`order_id`) BUCKETS 1\n"
+                + "PROPERTIES (\n"
+                + "  \"replication_num\" = \"1\",\n"
+                + "  \"enable_unique_key_merge_on_write\" = \"true\",\n"
+                + "  \"light_schema_change\" = \"true\",\n"
+                + "  \"store_row_column\" = \"true\"\n"
+                + ");");
     }
 
     @Test
     void testShortCircuitPointQueryKeepOlapScanWhenTableEmpty() {
+        Plan plan = rewrite("select * from tbl_point_query where `key` = 1");
+
+        
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
+        Assertions.assertTrue(plan.anyMatch(p -> p instanceof 
LogicalOlapScan));
+        Assertions.assertFalse(plan.anyMatch(p -> p instanceof 
LogicalEmptyRelation));
+    }
+
+    @Test
+    void testShortCircuitPointQueryKeepOlapScanWhenNoPartitionMatches() {
+        Plan plan = rewrite("select * from tbl_partitioned_point_query "
+                + "where order_id = 1 and pay_date = '2026-08-04'");
+
+        
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
+        Assertions.assertTrue(plan.anyMatch(p -> p instanceof LogicalOlapScan
+                && ((LogicalOlapScan) p).isPartitionPruned()
+                && ((LogicalOlapScan) p).getSelectedPartitionIds().isEmpty()
+                && ((LogicalOlapScan) p).hasPartitionPredicate()));
+        Assertions.assertFalse(plan.anyMatch(p -> p instanceof 
LogicalEmptyRelation));
+    }
+
+    @Test
+    void testNonPointQueryWithNoMatchingPartitionPrunesToEmptyRelation() {
+        Plan plan = rewrite("select * from tbl_partitioned_point_query "
+                + "where pay_date = '2026-08-04'");
+
+        
Assertions.assertFalse(connectContext.getStatementContext().isShortCircuitQuery());
+        Assertions.assertTrue(plan.anyMatch(p -> p instanceof 
LogicalEmptyRelation));
+        Assertions.assertFalse(plan.anyMatch(p -> p instanceof 
LogicalOlapScan));
+    }
+
+    @Test
+    void 
testShortCircuitPointQueryWithMatchingPartitionKeepsSelectedPartition() {
+        Plan plan = rewrite("select * from tbl_partitioned_point_query "
+                + "where order_id = 1 and pay_date = '2026-08-05'");
+
+        
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
+        Assertions.assertTrue(plan.anyMatch(p -> p instanceof LogicalOlapScan
+                && ((LogicalOlapScan) p).isPartitionPruned()
+                && !((LogicalOlapScan) 
p).getSelectedPartitionIds().isEmpty()));
+        Assertions.assertFalse(plan.anyMatch(p -> p instanceof 
LogicalEmptyRelation));
+    }
+
+    @Test
+    void testPointQueryWithManualPartitionDoesNotUseShortCircuit() {
+        rewrite("select * from tbl_partitioned_point_query 
partition(p20260805) "
+                + "where order_id = 1 and pay_date = '2026-08-05'");
+
+        
Assertions.assertFalse(connectContext.getStatementContext().isShortCircuitQuery());
+    }
+
+    @Test
+    void testPointQueryWithManualTabletDoesNotUseShortCircuit() throws 
Exception {
+        long tabletId = getTabletId("p20260805");
+        rewrite("select * from tbl_partitioned_point_query tablet(" + tabletId 
+ ") "
+                + "where order_id = 1 and pay_date = '2026-08-05'");
+
+        
Assertions.assertFalse(connectContext.getStatementContext().isShortCircuitQuery());
+    }
+
+    @Test
+    void testRemoteOlapTableDoesNotUseShortCircuit() throws Exception {
+        Database database = 
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
+        OlapTable table = (OlapTable) 
database.getTableOrMetaException("tbl_point_query");
+        RemoteOlapTable remoteTable = RemoteOlapTable.fromOlapTable(table);
+        LogicalOlapScan scan = new 
LogicalOlapScan(StatementScopeIdGenerator.newRelationId(), remoteTable);
+
+        
Assertions.assertTrue(connectContext.getSessionVariable().isEnableShortCircuitQuery());
+        Assertions.assertTrue(remoteTable.getEnableLightSchemaChange());
+        Assertions.assertTrue(remoteTable.getEnableUniqueKeyMergeOnWrite());
+        Assertions.assertTrue(remoteTable.storeRowColumn());
+        Assertions.assertFalse(new LogicalResultSinkToShortCircuitPointQuery()
+                .scanMatchShortCircuitCondition(scan));
+    }
+
+    private long getTabletId(String partitionName) throws Exception {
+        Database database = 
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
+        OlapTable table = (OlapTable) 
database.getTableOrMetaException("tbl_partitioned_point_query");
+        Tablet tablet = 
table.getPartition(partitionName).getBaseIndex().getTablets().iterator().next();
+        return tablet.getId();
+    }
+
+    private Plan rewrite(String sql) {
         boolean originRunningUnitTest = FeConstants.runningUnitTest;
         FeConstants.runningUnitTest = false;
         try {
-            String sql = "select * from tbl_point_query where `key` = 1";
-            Plan plan = PlanChecker.from(connectContext)
+            return PlanChecker.from(connectContext)
                     .analyze(sql)
                     .rewrite()
                     .getPlan();
-
-            // short-circuit flag should be set
-            
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
-            // should still keep scan node for point query path
-            Assertions.assertTrue(plan.anyMatch(p -> p instanceof 
LogicalOlapScan));
-            Assertions.assertFalse(plan.anyMatch(p -> p instanceof 
LogicalEmptyRelation));
         } finally {
             FeConstants.runningUnitTest = originRunningUnitTest;
         }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
index e24a57ee465..b43f0157073 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
@@ -19,10 +19,18 @@ package org.apache.doris.qe;
 
 import org.apache.doris.analysis.DescriptorTable;
 import org.apache.doris.analysis.Queriable;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.KeysType;
+import org.apache.doris.catalog.MaterializedIndex;
 import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.Partition;
+import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.RandomDistributionInfo;
+import org.apache.doris.catalog.SinglePartitionInfo;
 import org.apache.doris.planner.OlapScanNode;
 import org.apache.doris.planner.Planner;
 import org.apache.doris.thrift.TQueryOptions;
+import org.apache.doris.thrift.TStorageType;
 
 import org.apache.thrift.TDeserializer;
 import org.junit.jupiter.api.Assertions;
@@ -30,6 +38,7 @@ import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
 import java.util.Collections;
+import java.util.List;
 
 public class ShortCircuitQueryContextTest {
     private OlapTable table(String name, int schemaVersion) {
@@ -64,6 +73,26 @@ public class ShortCircuitQueryContextTest {
         Assertions.assertFalse(context.isReusable(connectContext(0)));
     }
 
+    @Test
+    public void testReusableRequiresSamePartitionTopologyVersion() {
+        long baseIndexId = 2L;
+        Column key = new Column("k", PrimitiveType.INT);
+        key.setIsKey(true);
+        List<Column> baseSchema = Collections.singletonList(key);
+        OlapTable table = new OlapTable(1L, "tbl", baseSchema, 
KeysType.DUP_KEYS,
+                new SinglePartitionInfo(), new RandomDistributionInfo(1));
+        table.setIndexMeta(baseIndexId, "tbl", baseSchema, 10, 0, (short) 1,
+                TStorageType.COLUMN, KeysType.DUP_KEYS);
+        table.setBaseIndexId(baseIndexId);
+        ShortCircuitQueryContext context = new ShortCircuitQueryContext(table, 
"tbl", 10, -1);
+
+        Assertions.assertTrue(context.isReusable(connectContext(-1)));
+        table.addPartition(new Partition(3L, "p1",
+                new MaterializedIndex(baseIndexId, 
MaterializedIndex.IndexState.NORMAL),
+                new RandomDistributionInfo(1)));
+        Assertions.assertFalse(context.isReusable(connectContext(-1)));
+    }
+
     @Test
     public void testSerializedQueryOptionsKeepBitmapOpCountVersion() throws 
Exception {
         TQueryOptions queryOptions = new SessionVariable().toThrift();
diff --git 
a/regression-test/data/point_query_p0/test_point_query_partition_not_exists.out 
b/regression-test/data/point_query_p0/test_point_query_partition_not_exists.out
new file mode 100644
index 00000000000..dd5d8eb103c
--- /dev/null
+++ 
b/regression-test/data/point_query_p0/test_point_query_partition_not_exists.out
@@ -0,0 +1,21 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !direct_no_partition --
+
+-- !prepared_no_partition_first --
+
+-- !prepared_no_partition_repeat --
+
+-- !prepared_added_partition --
+1      2026-08-04      added
+
+-- !prepared_existing_partition --
+2      2026-08-05      existing
+
+-- !direct_manual_partition_conflict --
+
+-- !direct_manual_tablet_conflict --
+
+-- !prepared_manual_partition_conflict --
+
+-- !prepared_manual_tablet_conflict --
+
diff --git 
a/regression-test/suites/point_query_p0/test_point_query_partition_not_exists.groovy
 
b/regression-test/suites/point_query_p0/test_point_query_partition_not_exists.groovy
new file mode 100644
index 00000000000..b373ec4d94b
--- /dev/null
+++ 
b/regression-test/suites/point_query_p0/test_point_query_partition_not_exists.groovy
@@ -0,0 +1,130 @@
+// 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.
+
+import com.mysql.cj.jdbc.ServerPreparedStatement
+
+import java.sql.Date
+
+suite("test_point_query_partition_not_exists") {
+    sql "CREATE DATABASE IF NOT EXISTS regression_test_point_query_p0"
+    sql "USE regression_test_point_query_p0"
+    sql "DROP TABLE IF EXISTS test_point_query_partition_not_exists"
+    sql """
+        CREATE TABLE test_point_query_partition_not_exists (
+            order_id BIGINT NOT NULL,
+            pay_date DATE NOT NULL,
+            value VARCHAR(30) NULL
+        ) ENGINE=OLAP
+        UNIQUE KEY(order_id, pay_date)
+        PARTITION BY RANGE(pay_date) (
+            PARTITION p20260805 VALUES [("2026-08-05"), ("2026-08-06")),
+            PARTITION p20260806 VALUES [("2026-08-06"), ("2026-08-07"))
+        )
+        DISTRIBUTED BY HASH(order_id) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1",
+            "enable_unique_key_merge_on_write" = "true",
+            "light_schema_change" = "true",
+            "store_row_column" = "true"
+        )
+    """
+    sql """
+        INSERT INTO test_point_query_partition_not_exists VALUES
+        (2, '2026-08-05', 'existing'),
+        (3, '2026-08-06', 'excluded')
+    """
+    sql "SET enable_nereids_planner = true"
+    sql "SET enable_fallback_to_original_planner = false"
+
+    explain {
+        sql """
+            SELECT * FROM test_point_query_partition_not_exists
+            WHERE order_id = 1 AND pay_date = '2026-08-04'
+        """
+        contains "SHORT-CIRCUIT"
+    }
+
+    order_qt_direct_no_partition """
+        SELECT * FROM test_point_query_partition_not_exists
+        WHERE order_id = 1 AND pay_date = '2026-08-04'
+    """
+
+    String url = getServerPrepareJdbcUrl(
+            context.config.jdbcUrl, "regression_test_point_query_p0", false)
+    connect(context.config.jdbcUser, context.config.jdbcPassword, url) {
+        def stmt = prepareStatement """
+            SELECT * FROM test_point_query_partition_not_exists
+            WHERE order_id = ? AND pay_date = ?
+        """
+        assertEquals(ServerPreparedStatement, stmt.class)
+
+        stmt.setLong(1, 1)
+        stmt.setDate(2, Date.valueOf("2026-08-04"))
+        qe_prepared_no_partition_first stmt
+        qe_prepared_no_partition_repeat stmt
+
+        sql """
+            ALTER TABLE test_point_query_partition_not_exists
+            ADD PARTITION p20260804 VALUES [("2026-08-04"), ("2026-08-05"))
+        """
+        sql "INSERT INTO test_point_query_partition_not_exists VALUES (1, 
'2026-08-04', 'added')"
+        stmt.setLong(1, 1)
+        stmt.setDate(2, Date.valueOf("2026-08-04"))
+        qe_prepared_added_partition stmt
+
+        stmt.setLong(1, 2)
+        stmt.setDate(2, Date.valueOf("2026-08-05"))
+        qe_prepared_existing_partition stmt
+        stmt.close()
+    }
+
+    def tablets = sql_return_maparray """
+        SHOW TABLETS FROM test_point_query_partition_not_exists 
PARTITION(p20260805)
+    """
+    def p20260805TabletId = tablets[0].TabletId
+
+    order_qt_direct_manual_partition_conflict """
+        SELECT * FROM test_point_query_partition_not_exists 
PARTITION(p20260805)
+        WHERE order_id = 3 AND pay_date = '2026-08-06'
+    """
+    order_qt_direct_manual_tablet_conflict """
+        SELECT * FROM test_point_query_partition_not_exists 
TABLET(${p20260805TabletId})
+        WHERE order_id = 3 AND pay_date = '2026-08-06'
+    """
+
+    connect(context.config.jdbcUser, context.config.jdbcPassword, url) {
+        def partitionStmt = prepareStatement """
+            SELECT * FROM test_point_query_partition_not_exists 
PARTITION(p20260805)
+            WHERE order_id = ? AND pay_date = ?
+        """
+        assertEquals(ServerPreparedStatement, partitionStmt.class)
+        partitionStmt.setLong(1, 3)
+        partitionStmt.setDate(2, Date.valueOf("2026-08-06"))
+        qe_prepared_manual_partition_conflict partitionStmt
+        partitionStmt.close()
+
+        def tabletStmt = prepareStatement """
+            SELECT * FROM test_point_query_partition_not_exists 
TABLET(${p20260805TabletId})
+            WHERE order_id = ? AND pay_date = ?
+        """
+        assertEquals(ServerPreparedStatement, tabletStmt.class)
+        tabletStmt.setLong(1, 3)
+        tabletStmt.setDate(2, Date.valueOf("2026-08-06"))
+        qe_prepared_manual_tablet_conflict tabletStmt
+        tabletStmt.close()
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to