Hi,
In the ATTACH PARTITION thread [1] Álvaro suggested exploring partial
indexes on system catalogs. The open question there was how to evaluate
the predicate during catalog maintenance without running the full
executor, while still representing the restriction in the catalogs.
Here is a sketch that seems to work, with a prototype behind it.
Evaluation. CatalogIndexInsert() is deliberately a cut-down version of
ExecInsertIndexTuples that avoids building an EState. Rather than give
that up, I keep a very small whitelist of predicate shapes that can be
evaluated by hand against the tuple: "oidcol <> 0" and "col IS NOT
NULL". For those the value comes from slot_getattr() and is compared
directly, with no expression machinery. Anything outside the whitelist
is an error.
Representation. The predicate is stored as the ordinary
pg_index.indpred, so the planner uses the partial index unchanged. The
restriction is not a new catalog field; it is an invariant checked in
two places that share the same whitelist: at CREATE/bootstrap time a
catalog index whose predicate is outside the whitelist is rejected, and
the maintenance evaluator re-checks the shape and errors out if it ever
sees anything else.
What the prototype does today, on top of master:
- a WHERE production in the bootstrap index grammar (bootparse.y), so
a predicate declared in the catalog headers survives initdb;
- the restricted evaluator and the creation-time guard above;
- genbki emits a partial unique index without a UNIQUE constraint,
since a constraint cannot be backed by a partial index.
With that, initdb is clean, the index carries the expected predicate,
the planner uses it, and amcheck (bt_index_check with heapallindexed)
passes. As a concrete case I split ConstraintRelidTypidNameIndexId into
two partial indexes, "WHERE conrelid <> 0" and "WHERE contypid <> 0":
each row falls into exactly one of them, no InvalidOid is stored, each
is unique on its own, and both pass amcheck.
What I have not done, and where I would value your view before going
further: actually replacing ConstraintRelidTypidNameIndexId reaches into
its callers (around thirty systable_beginscan sites), so I kept it as a
standalone demonstration for now. Tests and dropping a hardcoded
operator OID are still pending; pg_upgrade of a cluster carrying such
constraints migrates cleanly and amcheck passes on the new cluster.
The attached proof-of-concept patch (the core changes and a
catalog_partial_index regression test) reproduces everything above; it
applies on master and is meant for experimentation, not for commit.
Does this match what you had in mind for the evaluation and the
representation, or would you shape either differently?
[1] https://www.postgresql.org/message-id/arvPP0KhJvyf7sLA%40alvherre.pgsql
Thanks,
Manu
From 4766cdad179b7ab220d4abfde8af22add5c147c4 Mon Sep 17 00:00:00 2001
From: Manuel Reyes Bravo <[email protected]>
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 <[email protected]>
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