diff --git a/gpcontrib/Makefile b/gpcontrib/Makefile
index 2969194cfac..16ccc9f5ccd 100644
--- a/gpcontrib/Makefile
+++ b/gpcontrib/Makefile
@@ -22,7 +22,8 @@ ifeq "$(enable_debug_extensions)" "yes"
gp_legacy_string_agg \
gp_replica_check \
gp_toolkit \
- pg_hint_plan
+ pg_hint_plan \
+ reject_partition_fullscan
else
recurse_targets = gp_sparse_vector \
gp_distribution_policy \
diff --git a/gpcontrib/reject_partition_fullscan/Makefile b/gpcontrib/reject_partition_fullscan/Makefile
new file mode 100644
index 00000000000..65e08e4b186
--- /dev/null
+++ b/gpcontrib/reject_partition_fullscan/Makefile
@@ -0,0 +1,35 @@
+# 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.
+
+MODULE_big = reject_partition_fullscan
+OBJS = reject_partition_fullscan.o
+
+EXTENSION = reject_partition_fullscan
+DATA = reject_partition_fullscan--1.0.sql
+
+REGRESS = partition_fullscan_reject
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = gpcontrib/reject_partition_fullscan
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/gpcontrib/reject_partition_fullscan/reject_partition_fullscan--1.0.sql b/gpcontrib/reject_partition_fullscan/reject_partition_fullscan--1.0.sql
new file mode 100644
index 00000000000..c8093e8d18c
--- /dev/null
+++ b/gpcontrib/reject_partition_fullscan/reject_partition_fullscan--1.0.sql
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+/* gpcontrib/reject_partition_fullscan/reject_partition_fullscan--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION reject_partition_fullscan" to load this file. \quit
+
+-- Extension is loaded via shared_preload_libraries or LOAD command.
+-- No SQL objects needed; the planner hook and GUCs are registered
+-- automatically in _PG_init().
diff --git a/gpcontrib/reject_partition_fullscan/reject_partition_fullscan.c b/gpcontrib/reject_partition_fullscan/reject_partition_fullscan.c
new file mode 100644
index 00000000000..0670c8f6232
--- /dev/null
+++ b/gpcontrib/reject_partition_fullscan/reject_partition_fullscan.c
@@ -0,0 +1,488 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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.
+ *
+ * reject_partition_fullscan.c
+ *
+ * Extension to reject queries that scan all partitions of a
+ * partitioned table without effective partition pruning.
+ *
+ * This extension installs a planner_hook that wraps the standard planner
+ * (including ORCA). After the planner produces a PlannedStmt, the hook
+ * walks the plan tree looking for partition scan nodes that indicate no
+ * effective pruning occurred.
+ *
+ * Detection strategy (three complementary checks):
+ *
+ * 1) Nodes with PartitionPruneInfo (Planner Append with WHERE, or
+ * PartitionSelector in ORCA JOIN path): compare present_parts vs
+ * nparts. Exempt nodes with initial/exec pruning steps (runtime
+ * pruning capable).
+ *
+ * 2) Planner Append/MergeAppend without PartitionPruneInfo (no WHERE,
+ * WHERE 1=1, WHERE on non-partition-key): the Planner does not
+ * generate PartitionPruneInfo when there are no useful pruning quals.
+ * We detect these by checking if apprelids references a partitioned
+ * table RTE (relkind='p', inh=true).
+ *
+ * 3) ORCA DynamicSeqScan (and similar Dynamic nodes): ORCA never sets
+ * part_prune_info on Dynamic scan nodes. We detect full scans by
+ * comparing list_length(partOids) against the total partition count
+ * from catalog. Nodes with join_prune_paramids are skipped (JOIN
+ * dynamic pruning).
+ *
+ * GUC parameters (registered via DefineCustomXxxVariable):
+ * reject_partition_fullscan (bool, default true)
+ * partition_fullscan_threshold (int, default 0)
+ *
+ * IDENTIFICATION
+ * gpcontrib/reject_partition_fullscan/reject_partition_fullscan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits.h"
+#include "cdb/cdbllize.h"
+#include "nodes/bitmapset.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/cost.h"
+#include "optimizer/planner.h"
+#include "optimizer/walkers.h"
+#include "parser/parsetree.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+
+PG_MODULE_MAGIC;
+
+/* GUC variables */
+static bool reject_fullscan_enabled = true;
+static int fullscan_threshold = 0;
+
+/* Saved previous hook */
+static planner_hook_type prev_planner_hook = NULL;
+
+/* Forward declarations */
+void _PG_init(void);
+void _PG_fini(void);
+
+static PlannedStmt *rpf_planner_hook(Query *parse,
+ const char *query_string,
+ int cursorOptions,
+ ParamListInfo boundParams,
+ OptimizerOptions *optimizer_options);
+static void check_partition_fullscan(PlannedStmt *stmt);
+
+/* ----------------------------------------------------------------
+ * Utility: raise the rejection ERROR
+ * ----------------------------------------------------------------
+ */
+static void
+reject_fullscan(const char *nspname, const char *relname, int nparts)
+{
+ ereport(ERROR,
+ (errcode(ERRCODE_STATEMENT_TOO_COMPLEX),
+ errmsg("partitioned table \"%s.%s\" full partition "
+ "scan is not allowed, %d partitions would "
+ "be scanned",
+ nspname, relname, nparts),
+ errhint("Add a WHERE clause on the partition key "
+ "to enable partition pruning.")));
+}
+
+/* ----------------------------------------------------------------
+ * Check 1: Nodes with PartitionPruneInfo
+ * Used by: Planner Append (with pruning quals), PartitionSelector
+ * ----------------------------------------------------------------
+ */
+static PartitionPruneInfo *
+get_part_prune_info(Plan *plan)
+{
+ switch (nodeTag(plan))
+ {
+ case T_Append:
+ return ((Append *) plan)->part_prune_info;
+ case T_MergeAppend:
+ return ((MergeAppend *) plan)->part_prune_info;
+ case T_PartitionSelector:
+ return ((PartitionSelector *) plan)->part_prune_info;
+ case T_DynamicSeqScan:
+ return ((DynamicSeqScan *) plan)->part_prune_info;
+ case T_DynamicIndexScan:
+ return ((DynamicIndexScan *) plan)->part_prune_info;
+ case T_DynamicIndexOnlyScan:
+ return ((DynamicIndexOnlyScan *) plan)->part_prune_info;
+ case T_DynamicBitmapHeapScan:
+ return ((DynamicBitmapHeapScan *) plan)->part_prune_info;
+ case T_DynamicForeignScan:
+ return ((DynamicForeignScan *) plan)->part_prune_info;
+ default:
+ return NULL;
+ }
+}
+
+static void
+check_ppi_fullscan(PartitionPruneInfo *ppi, List *rtable)
+{
+ ListCell *lc1;
+
+ foreach(lc1, ppi->prune_infos)
+ {
+ List *prune_info_list = (List *) lfirst(lc1);
+ ListCell *lc2;
+
+ foreach(lc2, prune_info_list)
+ {
+ PartitionedRelPruneInfo *pinfo =
+ (PartitionedRelPruneInfo *) lfirst(lc2);
+ int total = pinfo->nparts;
+ int present = bms_num_members(pinfo->present_parts);
+ int threshold = fullscan_threshold;
+ bool do_reject = false;
+
+ if (total <= 1)
+ continue;
+
+ if (pinfo->initial_pruning_steps != NIL ||
+ pinfo->exec_pruning_steps != NIL)
+ continue;
+
+ if (threshold == 0)
+ do_reject = (present == total);
+ else
+ do_reject = (present > threshold);
+
+ if (do_reject)
+ {
+ RangeTblEntry *rte = rt_fetch(pinfo->rtindex, rtable);
+ char *relname = get_rel_name(rte->relid);
+ char *nspname = get_namespace_name(
+ get_rel_namespace(rte->relid));
+
+ reject_fullscan(nspname, relname, present);
+ }
+ }
+ }
+}
+
+/* ----------------------------------------------------------------
+ * Check 2: Planner Append/MergeAppend without PartitionPruneInfo
+ * Covers: no WHERE, WHERE 1=1, WHERE on non-partition-key
+ * ----------------------------------------------------------------
+ */
+static Index
+find_partitioned_parent_rti(Bitmapset *apprelids, List *rtable)
+{
+ int rti = -1;
+
+ while ((rti = bms_next_member(apprelids, rti)) >= 0)
+ {
+ RangeTblEntry *rte = rt_fetch(rti, rtable);
+
+ if (rte->rtekind == RTE_RELATION &&
+ rte->inh &&
+ rte->relkind == RELKIND_PARTITIONED_TABLE)
+ return (Index) rti;
+ }
+ return 0;
+}
+
+static int
+count_append_subplans(Plan *plan)
+{
+ if (IsA(plan, Append))
+ return list_length(((Append *) plan)->appendplans);
+ else if (IsA(plan, MergeAppend))
+ return list_length(((MergeAppend *) plan)->mergeplans);
+ return 0;
+}
+
+static void
+check_append_no_pruneinfo(Plan *plan, List *rtable)
+{
+ Bitmapset *apprelids = NULL;
+ Index parent_rti;
+ int nsubplans;
+ int threshold;
+
+ if (IsA(plan, Append))
+ apprelids = ((Append *) plan)->apprelids;
+ else if (IsA(plan, MergeAppend))
+ apprelids = ((MergeAppend *) plan)->apprelids;
+ else
+ return;
+
+ if (apprelids == NULL)
+ return;
+
+ parent_rti = find_partitioned_parent_rti(apprelids, rtable);
+ if (parent_rti == 0)
+ return;
+
+ nsubplans = count_append_subplans(plan);
+ threshold = fullscan_threshold;
+
+ if (nsubplans <= 1)
+ return;
+
+ if (threshold == 0 || nsubplans > threshold)
+ {
+ RangeTblEntry *rte = rt_fetch(parent_rti, rtable);
+ char *relname = get_rel_name(rte->relid);
+ char *nspname = get_namespace_name(
+ get_rel_namespace(rte->relid));
+
+ reject_fullscan(nspname, relname, nsubplans);
+ }
+}
+
+/* ----------------------------------------------------------------
+ * Check 3: ORCA DynamicSeqScan (and other Dynamic nodes)
+ * ORCA never sets part_prune_info on Dynamic scan nodes.
+ * We check partOids count vs total partition count.
+ * Nodes with join_prune_paramids are skipped (JOIN pruning).
+ * ----------------------------------------------------------------
+ */
+static void
+check_dynamic_scan_fullscan(Plan *plan, List *rtable)
+{
+ List *partOids = NIL;
+ List *join_prune_paramids = NIL;
+ Index scanrelid = 0;
+ int nscanned;
+ int ntotal;
+ int threshold;
+ RangeTblEntry *rte;
+ List *children;
+
+ switch (nodeTag(plan))
+ {
+ case T_DynamicSeqScan:
+ partOids = ((DynamicSeqScan *) plan)->partOids;
+ join_prune_paramids =
+ ((DynamicSeqScan *) plan)->join_prune_paramids;
+ scanrelid = ((DynamicSeqScan *) plan)->seqscan.scanrelid;
+ break;
+ case T_DynamicIndexScan:
+ partOids = ((DynamicIndexScan *) plan)->partOids;
+ join_prune_paramids =
+ ((DynamicIndexScan *) plan)->join_prune_paramids;
+ scanrelid = ((DynamicIndexScan *) plan)->indexscan.scan.scanrelid;
+ break;
+ case T_DynamicIndexOnlyScan:
+ partOids = ((DynamicIndexOnlyScan *) plan)->partOids;
+ join_prune_paramids =
+ ((DynamicIndexOnlyScan *) plan)->join_prune_paramids;
+ scanrelid =
+ ((DynamicIndexOnlyScan *) plan)->indexscan.scan.scanrelid;
+ break;
+ case T_DynamicBitmapHeapScan:
+ partOids = ((DynamicBitmapHeapScan *) plan)->partOids;
+ join_prune_paramids =
+ ((DynamicBitmapHeapScan *) plan)->join_prune_paramids;
+ scanrelid =
+ ((DynamicBitmapHeapScan *) plan)->bitmapheapscan.scan.scanrelid;
+ break;
+ case T_DynamicForeignScan:
+ partOids = ((DynamicForeignScan *) plan)->partOids;
+ join_prune_paramids =
+ ((DynamicForeignScan *) plan)->join_prune_paramids;
+ scanrelid =
+ ((DynamicForeignScan *) plan)->foreignscan.scan.scanrelid;
+ break;
+ default:
+ return;
+ }
+
+ /* Skip JOIN dynamic pruning -- runtime selection */
+ if (join_prune_paramids != NIL)
+ return;
+
+ nscanned = list_length(partOids);
+ if (nscanned <= 1)
+ return;
+
+ /* Verify this is a partitioned table */
+ rte = rt_fetch(scanrelid, rtable);
+ if (rte->rtekind != RTE_RELATION ||
+ rte->relkind != RELKIND_PARTITIONED_TABLE)
+ return;
+
+ threshold = fullscan_threshold;
+
+ if (threshold > 0)
+ {
+ /* Threshold mode: reject if scanned count > threshold */
+ if (nscanned > threshold)
+ {
+ char *relname = get_rel_name(rte->relid);
+ char *nspname = get_namespace_name(
+ get_rel_namespace(rte->relid));
+
+ reject_fullscan(nspname, relname, nscanned);
+ }
+ return;
+ }
+
+ /*
+ * threshold == 0: reject only true full scans (all partitions).
+ * Compare scanned count against total partition count from catalog.
+ * Use NoLock since the planner already holds a lock on this rel.
+ */
+ children = find_inheritance_children(rte->relid, NoLock);
+ ntotal = list_length(children);
+ list_free(children);
+
+ if (ntotal > 1 && nscanned >= ntotal)
+ {
+ char *relname = get_rel_name(rte->relid);
+ char *nspname = get_namespace_name(
+ get_rel_namespace(rte->relid));
+
+ reject_fullscan(nspname, relname, nscanned);
+ }
+}
+
+/* ----------------------------------------------------------------
+ * Plan tree walker
+ * ----------------------------------------------------------------
+ */
+typedef struct rpf_walker_context
+{
+ plan_tree_base_prefix base;
+ List *rtable;
+} rpf_walker_context;
+
+static bool
+rpf_plan_walker(Node *node, void *context)
+{
+ rpf_walker_context *ctx = (rpf_walker_context *) context;
+
+ if (node == NULL)
+ return false;
+
+ if (is_plan_node(node))
+ {
+ Plan *plan = (Plan *) node;
+ PartitionPruneInfo *ppi = get_part_prune_info(plan);
+
+ if (ppi != NULL)
+ {
+ /* Check 1: node has PartitionPruneInfo */
+ check_ppi_fullscan(ppi, ctx->rtable);
+ }
+ else if (IsA(node, Append) || IsA(node, MergeAppend))
+ {
+ /* Check 2: Planner Append without pruning info */
+ check_append_no_pruneinfo(plan, ctx->rtable);
+ }
+ else if (IsA(node, DynamicSeqScan) ||
+ IsA(node, DynamicIndexScan) ||
+ IsA(node, DynamicIndexOnlyScan) ||
+ IsA(node, DynamicBitmapHeapScan) ||
+ IsA(node, DynamicForeignScan))
+ {
+ /* Check 3: ORCA Dynamic scan without pruning info */
+ check_dynamic_scan_fullscan(plan, ctx->rtable);
+ }
+ }
+
+ return plan_tree_walker(node, rpf_plan_walker, context, true);
+}
+
+/* ----------------------------------------------------------------
+ * Entry point and planner hook
+ * ----------------------------------------------------------------
+ */
+static void
+check_partition_fullscan(PlannedStmt *stmt)
+{
+ rpf_walker_context ctx;
+
+ if (!reject_fullscan_enabled || !enable_partition_pruning)
+ return;
+
+ if (stmt->planTree == NULL)
+ return;
+
+ exec_init_plan_tree_base(&ctx.base, stmt);
+ ctx.rtable = stmt->rtable;
+
+ rpf_plan_walker((Node *) stmt->planTree, &ctx);
+}
+
+static PlannedStmt *
+rpf_planner_hook(Query *parse,
+ const char *query_string,
+ int cursorOptions,
+ ParamListInfo boundParams,
+ OptimizerOptions *optimizer_options)
+{
+ PlannedStmt *result;
+
+ if (prev_planner_hook)
+ result = prev_planner_hook(parse, query_string, cursorOptions,
+ boundParams, optimizer_options);
+ else
+ result = standard_planner(parse, query_string, cursorOptions,
+ boundParams, optimizer_options);
+
+ if (result != NULL)
+ check_partition_fullscan(result);
+
+ return result;
+}
+
+void
+_PG_init(void)
+{
+ DefineCustomBoolVariable(
+ "reject_partition_fullscan",
+ "Rejects queries that scan all partitions without pruning.",
+ "When enabled, queries on partitioned tables that cannot "
+ "prune any partition will be rejected with an error, "
+ "requiring a WHERE clause on the partition key.",
+ &reject_fullscan_enabled,
+ true,
+ PGC_USERSET,
+ GUC_EXPLAIN,
+ NULL, NULL, NULL);
+
+ DefineCustomIntVariable(
+ "partition_fullscan_threshold",
+ "Maximum partitions allowed after pruning before rejecting.",
+ "When reject_partition_fullscan is on, queries are rejected "
+ "if remaining partitions after pruning exceed this threshold. "
+ "0 means reject only when no pruning occurs at all.",
+ &fullscan_threshold,
+ 0, 0, INT_MAX,
+ PGC_USERSET,
+ GUC_EXPLAIN,
+ NULL, NULL, NULL);
+
+ prev_planner_hook = planner_hook;
+ planner_hook = rpf_planner_hook;
+}
+
+void
+_PG_fini(void)
+{
+ planner_hook = prev_planner_hook;
+}
diff --git a/gpcontrib/reject_partition_fullscan/reject_partition_fullscan.control b/gpcontrib/reject_partition_fullscan/reject_partition_fullscan.control
new file mode 100644
index 00000000000..7d925b76b84
--- /dev/null
+++ b/gpcontrib/reject_partition_fullscan/reject_partition_fullscan.control
@@ -0,0 +1,21 @@
+# 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.
+
+comment = 'reject queries that scan all partitions without pruning'
+default_version = '1.0'
+module_pathname = '$libdir/reject_partition_fullscan'
+relocatable = true
diff --git a/gpcontrib/reject_partition_fullscan/sql/partition_fullscan_reject.sql b/gpcontrib/reject_partition_fullscan/sql/partition_fullscan_reject.sql
new file mode 100644
index 00000000000..537d70f7501
--- /dev/null
+++ b/gpcontrib/reject_partition_fullscan/sql/partition_fullscan_reject.sql
@@ -0,0 +1,132 @@
+--
+-- 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.
+--
+
+--
+-- Test reject_partition_fullscan extension
+--
+-- Load extension via LOAD (alternative to shared_preload_libraries)
+LOAD 'reject_partition_fullscan';
+
+-- Create test partitioned table with 3 range partitions
+CREATE TABLE pfr_test (id int, dt date, val text)
+ PARTITION BY RANGE (dt);
+CREATE TABLE pfr_test_p1 PARTITION OF pfr_test
+ FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');
+CREATE TABLE pfr_test_p2 PARTITION OF pfr_test
+ FOR VALUES FROM ('2025-04-01') TO ('2025-07-01');
+CREATE TABLE pfr_test_p3 PARTITION OF pfr_test
+ FOR VALUES FROM ('2025-07-01') TO ('2025-10-01');
+
+-- Single-partition table for exemption test
+CREATE TABLE pfr_single (id int, dt date)
+ PARTITION BY RANGE (dt);
+CREATE TABLE pfr_single_p1 PARTITION OF pfr_single
+ FOR VALUES FROM ('2025-01-01') TO ('2025-12-31');
+
+-- ==============================
+-- Test 1: Basic rejection - no WHERE clause
+-- ==============================
+SET reject_partition_fullscan = on;
+SET partition_fullscan_threshold = 0;
+
+SELECT * FROM pfr_test;
+SELECT count(*) FROM pfr_test;
+
+-- ==============================
+-- Test 2: Pruning passes - WHERE on partition key
+-- ==============================
+SELECT * FROM pfr_test WHERE dt = '2025-02-01';
+SELECT * FROM pfr_test
+ WHERE dt >= '2025-01-01' AND dt < '2025-04-01';
+
+-- ==============================
+-- Test 3: WHERE not on partition key - should reject
+-- ==============================
+SELECT * FROM pfr_test WHERE val = 'x';
+SELECT * FROM pfr_test WHERE id = 1;
+
+-- ==============================
+-- Test 4: WHERE 1=1 - should reject (constant folded to NIL)
+-- ==============================
+SELECT * FROM pfr_test WHERE 1 = 1;
+SELECT * FROM pfr_test WHERE true;
+
+-- ==============================
+-- Test 5: GUC off - allow full scan
+-- ==============================
+SET reject_partition_fullscan = off;
+SELECT * FROM pfr_test;
+SET reject_partition_fullscan = on;
+
+-- ==============================
+-- Test 6: enable_partition_pruning=off exemption
+-- ==============================
+SET enable_partition_pruning = off;
+SELECT * FROM pfr_test;
+SET enable_partition_pruning = on;
+
+-- ==============================
+-- Test 7: Single-partition table exemption
+-- ==============================
+SELECT * FROM pfr_single;
+
+-- ==============================
+-- Test 8: Threshold mode
+-- ==============================
+SET partition_fullscan_threshold = 2;
+
+-- Pruned to 2 partitions, within threshold, should pass
+SELECT * FROM pfr_test
+ WHERE dt >= '2025-01-01' AND dt < '2025-07-01';
+
+-- All 3 partitions exceed threshold of 2, should reject
+SELECT * FROM pfr_test;
+
+SET partition_fullscan_threshold = 0;
+
+-- ==============================
+-- Test 9: Prepared statement with parameter (exemption)
+-- ==============================
+PREPARE pfr_q AS SELECT * FROM pfr_test WHERE dt = $1;
+EXECUTE pfr_q('2025-02-01');
+DEALLOCATE pfr_q;
+
+-- ==============================
+-- Test 10: UPDATE/DELETE without WHERE - should reject
+-- ==============================
+UPDATE pfr_test SET val = 'y';
+DELETE FROM pfr_test;
+
+-- UPDATE/DELETE with partition key - should pass
+UPDATE pfr_test SET val = 'y' WHERE dt = '2025-02-01';
+DELETE FROM pfr_test WHERE dt = '2025-02-01';
+
+-- ==============================
+-- Test 11: Subquery containing partitioned table
+-- ==============================
+SELECT * FROM (SELECT * FROM pfr_test) sub;
+
+-- ==============================
+-- Cleanup
+-- ==============================
+DROP TABLE pfr_test;
+DROP TABLE pfr_single;
+RESET reject_partition_fullscan;
+RESET partition_fullscan_threshold;
+RESET enable_partition_pruning;
diff --git a/pom.xml b/pom.xml
index 633b07afe8e..52e3bda9473 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1276,7 +1276,10 @@ code or new licensing patterns.
gpcontrib/gp_stats_collector/.clang-format
gpcontrib/gp_stats_collector/Makefile
-
contrib/pax_storage/src/test/**