From 4766cdad179b7ab220d4abfde8af22add5c147c4 Mon Sep 17 00:00:00 2001
From: Manuel Reyes Bravo <manuelreyesbravo@gmail.com>
Date: Thu, 1 Oct 2026 18:06:49 -0300
Subject: [PATCH 1/2] WIP: partial indexes on system catalogs (PoC)

Add a bootstrap WHERE grammar, a restricted predicate evaluator used by
CatalogIndexInsert that needs no EState, a creation-time guard sharing the
same whitelist, and genbki support for partial unique indexes that enforce
uniqueness without a formal constraint.  Exercises confrelid <> 0 and the
ConstraintRelidTypidNameIndexId split (relation vs type constraints).
---
 src/backend/bootstrap/bootparse.y   |  94 +++++++++++++++++++++-
 src/backend/bootstrap/bootscanner.l |   2 +
 src/backend/catalog/genbki.pl       |   5 +-
 src/backend/catalog/indexing.c      | 120 +++++++++++++++++++++++++++-
 src/backend/commands/indexcmds.c    |   9 +++
 src/include/catalog/indexing.h      |   2 +
 src/include/catalog/pg_constraint.h |   3 +
 7 files changed, 228 insertions(+), 7 deletions(-)

diff --git a/src/backend/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y
index 943ff4733d3..6f7a0bb6d9c 100644
--- a/src/backend/bootstrap/bootparse.y
+++ b/src/backend/bootstrap/bootparse.y
@@ -26,11 +26,17 @@
 #include "catalog/pg_class.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_type.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
+#include "access/htup_details.h"
+#include "access/table.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
+#include "nodes/value.h"
+#include "utils/fmgroids.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
 
 #include "bootparse.h"
 
@@ -75,6 +81,62 @@ do_end(void)
 
 static int num_columns_read = 0;
 
+/*
+ * Build a restricted predicate node for a partial index on a system catalog,
+ * declared in bootstrap as "WHERE <oidcolumn> <> <oidvalue>".  The bootstrap
+ * parser has no expression analyzer, so we hand-build the already-transformed
+ * OpExpr.  Only OID columns are accepted here; CatalogIndexPredSatisfied in
+ * indexing.c is the matching evaluator used to maintain the index.
+ */
+static Node *
+makeBootCatalogPredicate(Oid relid, char *colname, char *valstr)
+{
+	Relation	rel;
+	TupleDesc	tupdesc;
+	AttrNumber	attno = 0;
+	Oid			atttype = InvalidOid;
+	Var		   *var;
+	Const	   *con;
+	OpExpr	   *op;
+
+	rel = table_open(relid, NoLock);
+	tupdesc = RelationGetDescr(rel);
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+
+		if (strcmp(NameStr(att->attname), colname) == 0)
+		{
+			attno = att->attnum;
+			atttype = att->atttypid;
+			break;
+		}
+	}
+	table_close(rel, NoLock);
+
+	if (attno == 0)
+		elog(ERROR, "partial index predicate references unknown column \"%s\"",
+			 colname);
+	if (atttype != OIDOID)
+		elog(ERROR, "bootstrap partial index predicate supports only oid columns");
+
+	var = makeVar(1, attno, atttype, -1, InvalidOid, 0);
+	con = makeConst(OIDOID, -1, InvalidOid, sizeof(Oid),
+					ObjectIdGetDatum(atooid(valstr)), false, true);
+
+	op = makeNode(OpExpr);
+	op->opno = 608;				/* <>(oid,oid) */
+	op->opfuncid = F_OIDNE;
+	op->opresulttype = BOOLOID;
+	op->opretset = false;
+	op->opcollid = InvalidOid;
+	op->inputcollid = InvalidOid;
+	op->args = list_make2(var, con);
+	op->location = -1;
+
+	return (Node *) op;
+}
+
 %}
 
 %parse-param {yyscan_t yyscanner}
@@ -93,7 +155,7 @@ static int num_columns_read = 0;
 	Oid			oidval;
 }
 
-%type <list>  boot_index_params
+%type <list>  boot_index_params opt_where_clause
 %type <ielem> boot_index_param
 %type <str>   boot_ident
 %type <ival>  optbootstrap optsharedrelation boot_column_nullness
@@ -108,6 +170,8 @@ static int num_columns_read = 0;
 %token <kw> XDECLARE INDEX ON USING XBUILD INDICES UNIQUE XTOAST
 %token <kw> OBJ_ID XBOOTSTRAP XSHARED_RELATION XROWTYPE_OID
 %token <kw> XFORCE XNOT XNULL
+%token <kw> WHERE
+%token NEQ
 
 %start TopLevel
 
@@ -271,7 +335,7 @@ Boot_InsertStmt:
 		;
 
 Boot_DeclareIndexStmt:
-		  XDECLARE INDEX boot_ident oidspec ON boot_ident USING boot_ident LPAREN boot_index_params RPAREN
+		  XDECLARE INDEX boot_ident oidspec ON boot_ident USING boot_ident LPAREN boot_index_params RPAREN opt_where_clause
 				{
 					IndexStmt  *stmt = makeNode(IndexStmt);
 					Oid			relationId;
@@ -308,6 +372,15 @@ Boot_DeclareIndexStmt:
 					relationId = RangeVarGetRelid(stmt->relation, NoLock,
 												  false);
 
+					if ($12 != NIL)
+					{
+						stmt->whereClause =
+							makeBootCatalogPredicate(relationId,
+													 strVal(linitial($12)),
+													 strVal(lsecond($12)));
+						stmt->transformed = true;
+					}
+
 					DefineIndex(NULL,
 								relationId,
 								stmt,
@@ -325,7 +398,7 @@ Boot_DeclareIndexStmt:
 		;
 
 Boot_DeclareUniqueIndexStmt:
-		  XDECLARE UNIQUE INDEX boot_ident oidspec ON boot_ident USING boot_ident LPAREN boot_index_params RPAREN
+		  XDECLARE UNIQUE INDEX boot_ident oidspec ON boot_ident USING boot_ident LPAREN boot_index_params RPAREN opt_where_clause
 				{
 					IndexStmt  *stmt = makeNode(IndexStmt);
 					Oid			relationId;
@@ -362,6 +435,15 @@ Boot_DeclareUniqueIndexStmt:
 					relationId = RangeVarGetRelid(stmt->relation, NoLock,
 												  false);
 
+					if ($13 != NIL)
+					{
+						stmt->whereClause =
+							makeBootCatalogPredicate(relationId,
+													 strVal(linitial($13)),
+													 strVal(lsecond($13)));
+						stmt->transformed = true;
+					}
+
 					DefineIndex(NULL,
 								relationId,
 								stmt,
@@ -405,6 +487,12 @@ boot_index_params:
 		| boot_index_param							{ $$ = list_make1($1); }
 		;
 
+opt_where_clause:
+		WHERE boot_ident NEQ boot_ident
+				{ $$ = list_make2(makeString($2), makeString($4)); }
+		| /* EMPTY */								{ $$ = NIL; }
+		;
+
 boot_index_param:
 		boot_ident boot_ident
 				{
diff --git a/src/backend/bootstrap/bootscanner.l b/src/backend/bootstrap/bootscanner.l
index 9674f2795d1..622dfdc8706 100644
--- a/src/backend/bootstrap/bootscanner.l
+++ b/src/backend/bootstrap/bootscanner.l
@@ -90,6 +90,7 @@ _null_			{ return NULLVAL; }
 
 ","				{ return COMMA; }
 "="				{ return EQUALS; }
+"<>"			{ return NEQ; }
 "("				{ return LPAREN; }
 ")"				{ return RPAREN; }
 
@@ -109,6 +110,7 @@ toast			{ yylval->kw = "toast"; return XTOAST; }
 FORCE			{ yylval->kw = "FORCE"; return XFORCE; }
 NOT				{ yylval->kw = "NOT"; return XNOT; }
 NULL			{ yylval->kw = "NULL"; return XNULL; }
+WHERE			{ yylval->kw = "WHERE"; return WHERE; }
 
 {id}			{
 					yylval->str = pstrdup(yytext);
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 86f3135f9c7..f36e23aab3d 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -149,7 +149,10 @@ foreach my $header (@ARGV)
 		  $index->{index_decl};
 		$oidcounts{ $index->{index_oid} }++;
 
-		if ($index->{is_unique})
+		# A unique index carrying a partial predicate still enforces uniqueness
+		# over the matching rows, but it cannot back a UNIQUE or PRIMARY KEY
+		# constraint, so don't generate one for it.
+		if ($index->{is_unique} && $index->{index_decl} !~ /\bWHERE\b/i)
 		{
 			push @system_constraints,
 			  sprintf "ALTER TABLE %s ADD %s USING INDEX %s;",
diff --git a/src/backend/catalog/indexing.c b/src/backend/catalog/indexing.c
index fd7d2ec0e3a..6fdb1b0f049 100644
--- a/src/backend/catalog/indexing.c
+++ b/src/backend/catalog/indexing.c
@@ -22,6 +22,8 @@
 #include "catalog/index.h"
 #include "catalog/indexing.h"
 #include "executor/executor.h"
+#include "nodes/nodeFuncs.h"
+#include "utils/fmgroids.h"
 #include "utils/rel.h"
 
 
@@ -64,6 +66,108 @@ CatalogCloseIndexes(CatalogIndexState indstate)
 	pfree(indstate);
 }
 
+/*
+ * catalog_pred_clause_supported - is this predicate clause one we can maintain?
+ *
+ * Only a tiny whitelist of clause shapes can be evaluated during catalog
+ * maintenance without building an EState: an OID inequality "oidcol <> k" and
+ * "col IS NOT NULL".  Shared by the creation-time check and the maintenance
+ * evaluator so the two can never drift apart.
+ */
+static bool
+catalog_pred_clause_supported(Node *clause)
+{
+	if (IsA(clause, OpExpr))
+	{
+		OpExpr	   *op = (OpExpr *) clause;
+
+		if (op->opfuncid != F_OIDNE || list_length(op->args) != 2)
+			return false;
+		/* literals arrive wrapped in an implicit int4->oid cast */
+		return (IsA(strip_implicit_coercions((Node *) linitial(op->args)), Var) &&
+				IsA(strip_implicit_coercions((Node *) lsecond(op->args)), Const));
+	}
+	if (IsA(clause, NullTest))
+	{
+		NullTest   *nt = (NullTest *) clause;
+
+		return (nt->nulltesttype == IS_NOT_NULL && IsA(nt->arg, Var));
+	}
+	return false;
+}
+
+/*
+ * CheckCatalogIndexPredicate - reject predicates we could not maintain
+ *
+ * Called at index creation time for any partial index on a system catalog, so
+ * that such an index can never be built carrying a predicate that
+ * CatalogIndexInsert would be unable to evaluate later.
+ */
+void
+CheckCatalogIndexPredicate(List *predicate)
+{
+	ListCell   *lc;
+
+	foreach(lc, predicate)
+	{
+		if (!catalog_pred_clause_supported((Node *) lfirst(lc)))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("unsupported predicate for a partial index on a system catalog"),
+					 errdetail("Only \"oidcolumn <> 0\" and \"column IS NOT NULL\" predicates are supported.")));
+	}
+}
+
+/*
+ * CatalogIndexPredSatisfied - evaluate a restricted partial-index predicate
+ *
+ * Evaluates the predicate against the tuple in "slot" without building an
+ * EState.  Only the shapes accepted by catalog_pred_clause_supported() can
+ * occur here (enforced at creation by CheckCatalogIndexPredicate); anything
+ * else is a should-not-happen, so we fail loudly rather than guess.
+ *
+ * The predicate is an implicit-AND list of clauses.
+ */
+static bool
+CatalogIndexPredSatisfied(List *predicate, TupleTableSlot *slot)
+{
+	ListCell   *lc;
+
+	foreach(lc, predicate)
+	{
+		Node	   *clause = (Node *) lfirst(lc);
+		Var		   *var;
+		Datum		val;
+		bool		isnull;
+
+		if (!catalog_pred_clause_supported(clause))
+			elog(ERROR, "unsupported predicate on system catalog index");
+
+		if (IsA(clause, OpExpr))
+		{
+			OpExpr	   *op = (OpExpr *) clause;
+			Const	   *con;
+
+			var = (Var *) strip_implicit_coercions((Node *) linitial(op->args));
+			con = (Const *) strip_implicit_coercions((Node *) lsecond(op->args));
+			val = slot_getattr(slot, var->varattno, &isnull);
+			if (isnull || con->constisnull)
+				return false;
+			if (DatumGetObjectId(val) == DatumGetObjectId(con->constvalue))
+				return false;	/* "<>" fails when the values are equal */
+		}
+		else					/* NullTest IS NOT NULL */
+		{
+			var = (Var *) ((NullTest *) clause)->arg;
+			(void) slot_getattr(slot, var->varattno, &isnull);
+			if (isnull)
+				return false;
+		}
+	}
+
+	return true;
+}
+
 /*
  * CatalogIndexInsert - insert index entries for one catalog tuple
  *
@@ -129,11 +233,12 @@ CatalogIndexInsert(CatalogIndexState indstate, HeapTuple heapTuple,
 			continue;
 
 		/*
-		 * Expressional and partial indexes on system catalogs are not
-		 * supported, nor exclusion constraints, nor deferred uniqueness
+		 * Expressional indexes on system catalogs are not supported, nor
+		 * exclusion constraints, nor deferred uniqueness.  Partial indexes
+		 * are supported for a restricted set of predicates, evaluated below
+		 * by CatalogIndexPredSatisfied without building an EState.
 		 */
 		Assert(indexInfo->ii_Expressions == NIL);
-		Assert(indexInfo->ii_Predicate == NIL);
 		Assert(indexInfo->ii_ExclusionOps == NULL);
 		Assert(index->rd_index->indimmediate);
 		Assert(indexInfo->ii_NumIndexKeyAttrs != 0);
@@ -154,6 +259,15 @@ CatalogIndexInsert(CatalogIndexState indstate, HeapTuple heapTuple,
 		if (onlySummarized && !indexInfo->ii_Summarizing)
 			continue;
 
+		/*
+		 * For a partial index, skip tuples that don't satisfy the predicate.
+		 * We evaluate it without an executor; only a whitelisted set of
+		 * predicate shapes is allowed (see CatalogIndexPredSatisfied).
+		 */
+		if (indexInfo->ii_Predicate != NIL &&
+			!CatalogIndexPredSatisfied(indexInfo->ii_Predicate, slot))
+			continue;
+
 		/*
 		 * FormIndexDatum fills in its values and isnull parameters with the
 		 * appropriate values for the column(s) of the index.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index af03cc7cb08..a364a2807ad 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -933,6 +933,15 @@ DefineIndex(ParseState *pstate,
 							  amissummarizing,
 							  stmt->iswithoutoverlaps);
 
+	/*
+	 * Partial indexes on system catalogs may only use predicate shapes that
+	 * catalog maintenance can evaluate without an executor (see
+	 * CatalogIndexPredSatisfied).  Reject anything else up front, so a catalog
+	 * index can never carry a predicate we would be unable to maintain.
+	 */
+	if (indexInfo->ii_Predicate != NIL && IsCatalogRelationOid(tableId))
+		CheckCatalogIndexPredicate(indexInfo->ii_Predicate);
+
 	typeIds = palloc_array(Oid, numberOfAttributes);
 	collationIds = palloc_array(Oid, numberOfAttributes);
 	opclassIds = palloc_array(Oid, numberOfAttributes);
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 58fc185d0d8..74fb109834c 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -51,4 +51,6 @@ extern void CatalogTupleUpdateWithInfo(Relation heapRel,
 									   CatalogIndexState indstate);
 extern void CatalogTupleDelete(Relation heapRel, const ItemPointerData *tid);
 
+extern void CheckCatalogIndexPredicate(List *predicate);
+
 #endif							/* INDEXING_H */
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 1b7fedf1750..84da437cea5 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -185,6 +185,9 @@ DECLARE_UNIQUE_INDEX(pg_constraint_conrelid_contypid_conname_index, 2665, Constr
 DECLARE_INDEX(pg_constraint_contypid_index, 2666, ConstraintTypidIndexId, pg_constraint, btree(contypid oid_ops));
 DECLARE_UNIQUE_INDEX_PKEY(pg_constraint_oid_index, 2667, ConstraintOidIndexId, pg_constraint, btree(oid oid_ops));
 DECLARE_INDEX(pg_constraint_conparentid_index, 2579, ConstraintParentIndexId, pg_constraint, btree(conparentid oid_ops));
+DECLARE_INDEX(pg_constraint_confrelid_partial_index, 580, ConstraintConfrelidPartialIndexId, pg_constraint, btree(confrelid oid_ops) WHERE confrelid <> 0);
+DECLARE_UNIQUE_INDEX(pg_constraint_conrelid_conname_index, 581, ConstraintRelidConnameIndexId, pg_constraint, btree(conrelid oid_ops, conname name_ops) WHERE conrelid <> 0);
+DECLARE_UNIQUE_INDEX(pg_constraint_contypid_conname_index, 582, ConstraintTypidConnameIndexId, pg_constraint, btree(contypid oid_ops, conname name_ops) WHERE contypid <> 0);
 
 MAKE_SYSCACHE(CONSTROID, pg_constraint_oid_index, 16);
 
-- 
2.55.0


From 5adda5522473508742dd52a6f6c4fd4d5102e1e2 Mon Sep 17 00:00:00 2001
From: Manuel Reyes Bravo <manuelreyesbravo@gmail.com>
Date: Thu, 1 Oct 2026 18:30:46 -0300
Subject: [PATCH 2/2] Add regression test for partial indexes on system
 catalogs

Covers the creation-time guard that rejects non-whitelisted predicates and
the maintenance of a partial catalog index through CatalogIndexInsert.
---
 .../expected/catalog_partial_index.out        | 38 +++++++++++++++++++
 src/test/regress/parallel_schedule            |  4 ++
 .../regress/sql/catalog_partial_index.sql     | 35 +++++++++++++++++
 3 files changed, 77 insertions(+)
 create mode 100644 src/test/regress/expected/catalog_partial_index.out
 create mode 100644 src/test/regress/sql/catalog_partial_index.sql

diff --git a/src/test/regress/expected/catalog_partial_index.out b/src/test/regress/expected/catalog_partial_index.out
new file mode 100644
index 00000000000..4838597d0c7
--- /dev/null
+++ b/src/test/regress/expected/catalog_partial_index.out
@@ -0,0 +1,38 @@
+--
+-- Partial indexes on system catalogs
+--
+-- A partial index on a catalog is maintained by CatalogIndexInsert() without
+-- building an executor, so only a small whitelist of predicate shapes is
+-- allowed: an OID inequality "oidcol <> 0" and "col IS NOT NULL".
+--
+-- A predicate outside the whitelist is rejected when the index is created.
+SET allow_system_table_mods = on;
+CREATE INDEX cpi_bad ON pg_constraint (conrelid) WHERE conname <> 'x';
+ERROR:  unsupported predicate for a partial index on a system catalog
+DETAIL:  Only "oidcolumn <> 0" and "column IS NOT NULL" predicates are supported.
+-- OID inequality and IS NOT NULL are accepted.
+CREATE INDEX cpi_relid ON pg_constraint (conrelid, conname) WHERE conrelid <> 0;
+CREATE INDEX cpi_typid ON pg_constraint (contypid, conname)
+	WHERE contypid IS NOT NULL;
+RESET allow_system_table_mods;
+-- The partial index is maintained: a foreign key (conrelid <> 0) is indexed,
+-- and the row is reachable through the partial index with seqscans disabled.
+CREATE TABLE cpi_p (id int PRIMARY KEY);
+CREATE TABLE cpi_c (a int REFERENCES cpi_p(id));
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+SELECT conname FROM pg_constraint
+	WHERE conrelid = 'cpi_c'::regclass AND conrelid <> 0
+	ORDER BY conname;
+   conname    
+--------------
+ cpi_c_a_fkey
+(1 row)
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+-- Cleanup.
+DROP TABLE cpi_c, cpi_p;
+SET allow_system_table_mods = on;
+DROP INDEX cpi_relid, cpi_typid;
+RESET allow_system_table_mods;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 75063f87a4a..6e55daf5d5d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -143,6 +143,10 @@ test: event_trigger_login
 # this test also uses event triggers, so likewise run it by itself
 test: fast_default
 
+# catalog_partial_index creates indexes on a system catalog with
+# allow_system_table_mods, so run it by itself.
+test: catalog_partial_index
+
 # run tablespace test at the end because it drops the tablespace created during
 # setup that other tests may use.
 test: tablespace
diff --git a/src/test/regress/sql/catalog_partial_index.sql b/src/test/regress/sql/catalog_partial_index.sql
new file mode 100644
index 00000000000..8739c627a3b
--- /dev/null
+++ b/src/test/regress/sql/catalog_partial_index.sql
@@ -0,0 +1,35 @@
+--
+-- Partial indexes on system catalogs
+--
+-- A partial index on a catalog is maintained by CatalogIndexInsert() without
+-- building an executor, so only a small whitelist of predicate shapes is
+-- allowed: an OID inequality "oidcol <> 0" and "col IS NOT NULL".
+--
+
+-- A predicate outside the whitelist is rejected when the index is created.
+SET allow_system_table_mods = on;
+CREATE INDEX cpi_bad ON pg_constraint (conrelid) WHERE conname <> 'x';
+
+-- OID inequality and IS NOT NULL are accepted.
+CREATE INDEX cpi_relid ON pg_constraint (conrelid, conname) WHERE conrelid <> 0;
+CREATE INDEX cpi_typid ON pg_constraint (contypid, conname)
+	WHERE contypid IS NOT NULL;
+RESET allow_system_table_mods;
+
+-- The partial index is maintained: a foreign key (conrelid <> 0) is indexed,
+-- and the row is reachable through the partial index with seqscans disabled.
+CREATE TABLE cpi_p (id int PRIMARY KEY);
+CREATE TABLE cpi_c (a int REFERENCES cpi_p(id));
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+SELECT conname FROM pg_constraint
+	WHERE conrelid = 'cpi_c'::regclass AND conrelid <> 0
+	ORDER BY conname;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+-- Cleanup.
+DROP TABLE cpi_c, cpi_p;
+SET allow_system_table_mods = on;
+DROP INDEX cpi_relid, cpi_typid;
+RESET allow_system_table_mods;
-- 
2.55.0

