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

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


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

commit 5f4cc0badd7b0bd129d2a20ea4bec7f044723463
Author: morrySnow <[email protected]>
AuthorDate: Mon Sep 21 10:38:11 2026 +0800

    branch:4.2: [fix](point query) Keep point-query scan when partition pruning 
is empty #67161 (#68293)
    
    picked from #67161
    
    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       |  16 ++-
 .../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  | 130 +++++++++++++++++----
 .../doris/qe/ShortCircuitQueryContextTest.java     |  31 +++++
 .../test_point_query_partition_not_exists.out      |  20 ++++
 .../test_point_query_partition_not_exists.groovy   | 130 +++++++++++++++++++++
 11 files changed, 383 insertions(+), 38 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 3fe0ba94c8e..ae7dd033d97 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
@@ -121,6 +121,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;
@@ -183,6 +184,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();
@@ -1286,8 +1291,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.
@@ -1303,6 +1311,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);
@@ -1588,6 +1597,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;
@@ -2018,6 +2035,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
@@ -2172,6 +2190,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 4f5dbb345c9..51bdc44b66b 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;
@@ -32,6 +33,7 @@ import 
org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.qe.ConnectContext.ConnectType;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 
@@ -61,7 +63,8 @@ public class LogicalResultSinkToShortCircuitPointQuery 
implements RewriteRuleFac
                         && expression.child(1).isLiteral());
     }
 
-    private boolean scanMatchShortCircuitCondition(LogicalOlapScan olapScan) {
+    @VisibleForTesting
+    boolean scanMatchShortCircuitCondition(LogicalOlapScan olapScan) {
         ConnectContext connectContext = ConnectContext.get();
         if (!connectContext.getSessionVariable().isEnableShortCircuitQuery()) {
             return false;
@@ -78,7 +81,18 @@ public class LogicalResultSinkToShortCircuitPointQuery 
implements RewriteRuleFac
         if (connectContext.getConnectType() == ConnectType.ARROW_FLIGHT_SQL) {
             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 b0cfde50962..845449952bd 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 5bedc88e811..203deb626a8 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.StmtType;
 import org.apache.doris.analysis.TableScanParams;
 import org.apache.doris.nereids.StatementContext;
@@ -41,6 +40,8 @@ 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 java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -170,11 +171,14 @@ public class ExecuteCommand extends Command {
         preparedStmtCtx.shortCircuitQueryContext = Optional.empty();
         statementContext.setShortCircuitQueryContext(null);
         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 1602b641972..4cda24153d3 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
@@ -56,6 +56,7 @@ public class ShortCircuitQueryContext {
     public final int schemaVersion;
     public final OlapTable tbl;
     private final long fileCacheQueryLimitBytes;
+    private final long partitionTopologyVersion;
 
     public final OlapScanNode scanNode;
     public final Queriable analzyedQuery;
@@ -109,6 +110,7 @@ public class ShortCircuitQueryContext {
         this.scanNode = olapScanNode;
         this.tbl = this.scanNode.getOlapTable();
         this.schemaVersion = this.tbl.getBaseSchemaVersion();
+        this.partitionTopologyVersion = this.tbl.getPartitionTopologyVersion();
         this.analzyedQuery = analzyedQuery;
     }
 
@@ -122,13 +124,15 @@ public class ShortCircuitQueryContext {
         this.tbl = tbl;
         this.schemaVersion = schemaVersion;
         this.fileCacheQueryLimitBytes = fileCacheQueryLimitBytes;
+        this.partitionTopologyVersion = tbl.getPartitionTopologyVersion();
         this.scanNode = null;
         this.analzyedQuery = null;
     }
 
     public boolean isReusable(ConnectContext ctx) {
         return this.tbl.getBaseSchemaVersion() == this.schemaVersion
-                && 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 b349cf9160d..d8e228e65e8 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
@@ -1415,10 +1415,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 50fab95e69c..e7ef1774f43 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
@@ -78,6 +78,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 a9b2815b3d9..512033b35a5 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;
@@ -35,8 +41,9 @@ import java.lang.reflect.Field;
 /**
  * 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 {
@@ -57,24 +64,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;
         }
@@ -109,16 +207,4 @@ class ShortCircuitPointQueryTest extends TestWithFeService
         
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
     }
 
-    private Plan rewrite(String sql) {
-        boolean originRunningUnitTest = FeConstants.runningUnitTest;
-        FeConstants.runningUnitTest = false;
-        try {
-            return PlanChecker.from(connectContext)
-                    .analyze(sql)
-                    .rewrite()
-                    .getPlan();
-        } 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 cdbd6491e5e..f5a13b13cdd 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
@@ -17,12 +17,23 @@
 
 package org.apache.doris.qe;
 
+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.thrift.TStorageType;
 
 import org.junit.jupiter.api.Assertions;
 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) {
         OlapTable table = Mockito.spy(new OlapTable());
@@ -55,4 +66,24 @@ 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, 
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)));
+    }
 }
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..8c5343b4dcf
--- /dev/null
+++ 
b/regression-test/data/point_query_p0/test_point_query_partition_not_exists.out
@@ -0,0 +1,20 @@
+-- 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