From 4766cdad179b7ab220d4abfde8af22add5c147c4 Mon Sep 17 00:00:00 2001 From: Manuel Reyes Bravo 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 <> ". 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 boot_index_params +%type boot_index_params opt_where_clause %type boot_index_param %type boot_ident %type optbootstrap optsharedrelation boot_column_nullness @@ -108,6 +170,8 @@ static int num_columns_read = 0; %token XDECLARE INDEX ON USING XBUILD INDICES UNIQUE XTOAST %token OBJ_ID XBOOTSTRAP XSHARED_RELATION XROWTYPE_OID %token XFORCE XNOT XNULL +%token 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 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