From 5d26d70e9d84a41899e4704335ac75e685f619f4 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Fri, 11 Sep 2026 17:01:08 +0900 Subject: [PATCH v2 2/2] Invalidate RI call information when casts change Changing a cast need not modify pg_constraint, so the RI comparison cache can keep using the old cast function after its replacement. Dropping that function can then make an UPDATE fail with a cache lookup error. The fast-path foreign key checks added in PostgreSQL 19 also use this cache and copy its call information into their own metadata, exposing the problem on INSERT as well. Invalidate both caches on CASTSOURCETARGET changes. Keep comparison call information separate from its hash entry and defer releasing invalidated objects until transaction end, as already done for fast-path metadata. A cast can run DDL and reenter RI checks, so invalidation must not free or overwrite call information still used by an outer comparison. Build new comparison and fast-path call information in transaction-owned contexts and reparent them only when construction succeeds, so failed rebuilds do not leak backend memory. Test replacement after cache warmup using a committed row, invalid-key rejection, rollback restoring the old cast, and invalidation with a nested comparison of another committed row. Also check cleanup after failed fast-path rebuilds. Author: Nikolay Samokhvalov Co-authored-by: Amit Langote Discussion: https://postgr.es/m/CAM527d9BgPjeOOYmbCBTd57R145qHCk-dzw9qNq+nOrDq1j__A@mail.gmail.com Backpatch-through: 14 --- src/backend/utils/adt/ri_triggers.c | 130 ++++++++++++++++------ src/test/regress/expected/foreign_key.out | 117 +++++++++++++++++++ src/test/regress/sql/foreign_key.sql | 110 ++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 4 files changed, 322 insertions(+), 36 deletions(-) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index c62f7dc2d06..1811a3c2ae4 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -165,7 +165,7 @@ typedef struct FastPathMeta * fn_mcxt for the cached FmgrInfos above. Cast and equality functions * (e.g. record_eq()) use fn_mcxt as scratch space, caching state there * and keeping a pointer to it in FmgrInfo.fn_extra. Give them a context - * of their own, created with this struct and destroyed with it in + * of their own, which also owns this struct and is destroyed by * AtEOXact_RI(). * * Note this context must not be reset while the FmgrInfos remain in use, @@ -208,15 +208,25 @@ typedef struct RI_CompareKey Oid typeid; /* the data type to apply it to */ } RI_CompareKey; +/* + * Cached call information is detached on invalidation, but kept until the end + * of the transaction in case an active comparison still references it. + */ +typedef struct RI_CompareInfo +{ + FmgrInfo eq_opr_finfo; /* call info for equality fn */ + FmgrInfo cast_func_finfo; /* in case we must coerce input */ + MemoryContext context; + struct RI_CompareInfo *next_dead; +} RI_CompareInfo; + /* * RI_CompareHashEntry */ typedef struct RI_CompareHashEntry { RI_CompareKey key; - bool valid; /* successfully initialized? */ - FmgrInfo eq_opr_finfo; /* call info for equality fn */ - FmgrInfo cast_func_finfo; /* in case we must coerce input */ + RI_CompareInfo *info; /* NULL if invalid */ } RI_CompareHashEntry; /* @@ -316,6 +326,9 @@ static bool ri_fastpath_flushing = false; */ static FastPathMeta *ri_fpmeta_dead_list = NULL; +/* Comparison call information detached by InvalidateCastCacheCallBack(). */ +static RI_CompareInfo *ri_compare_dead_list = NULL; + /* * Local function prototypes */ @@ -343,11 +356,13 @@ static bool ri_CompareWithCast(Oid eq_opr, Oid typeid, Oid collid, Datum lhs, Datum rhs); static void ri_InitHashTables(void); +static void InvalidateCastCacheCallBack(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); static void InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue); static SPIPlanPtr ri_FetchPreparedPlan(RI_QueryKey *key); static void ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan); -static RI_CompareHashEntry *ri_HashCompareOp(Oid eq_opr, Oid typeid); +static RI_CompareInfo *ri_HashCompareOp(Oid eq_opr, Oid typeid); static void ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname, int tgkind); @@ -2666,6 +2681,34 @@ InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, } +/* + * Cast changes can affect any comparison or fast-path entry. Do not free or + * overwrite call information here: a cast can execute DDL and reenter RI checks + * while an outer call is still using it. AtEOXact_RI() releases detached data. + */ +static void +InvalidateCastCacheCallBack(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + RI_CompareHashEntry *entry; + + hash_seq_init(&status, ri_compare_cache); + while ((entry = hash_seq_search(&status)) != NULL) + { + if (entry->info != NULL) + { + entry->info->next_dead = ri_compare_dead_list; + ri_compare_dead_list = entry->info; + entry->info = NULL; + } + } + + /* Fast-path metadata contains copies of the cached call information. */ + InvalidateConstraintCacheCallBack(arg, cacheid, 0); +} + + /* * Prepare execution plan for a query to enforce an RI restriction */ @@ -3674,24 +3717,23 @@ ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo, Relation fk_rel, Relation idx_rel) { FastPathMeta *fpmeta; - MemoryContext oldcxt = MemoryContextSwitchTo(TopMemoryContext); + MemoryContext context; Assert(riinfo != NULL && riinfo->valid); Assert(riinfo->fpmeta == NULL); - fpmeta = palloc_object(FastPathMeta); - fpmeta->next_dead = NULL; - - /* Scratch context for the cached FmgrInfos' fn_mcxt; see FastPathMeta. */ - fpmeta->scratch_cxt = AllocSetContextCreate(TopMemoryContext, - "RI fast-path finfo scratch", - ALLOCSET_SMALL_SIZES); + /* Keep incomplete metadata subject to normal error cleanup. */ + context = AllocSetContextCreate(CurTransactionContext, + "RI fast-path finfo scratch", + ALLOCSET_SMALL_SIZES); + fpmeta = MemoryContextAllocZero(context, sizeof(FastPathMeta)); + fpmeta->scratch_cxt = context; for (int i = 0; i < riinfo->nkeys; i++) { Oid eq_opr = riinfo->pf_eq_oprs[i]; Oid typeid = RIAttType(fk_rel, riinfo->fk_attnums[i]); Oid lefttype; - RI_CompareHashEntry *entry = ri_HashCompareOp(eq_opr, typeid); + RI_CompareInfo *entry = ri_HashCompareOp(eq_opr, typeid); int idx_col; /* @@ -3724,8 +3766,8 @@ ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo, &fpmeta->subtypes[i]); } + MemoryContextSetParent(context, TopMemoryContext); riinfo->fpmeta = fpmeta; - MemoryContextSwitchTo(oldcxt); } /* @@ -3996,6 +4038,10 @@ ri_InitHashTables(void) ri_compare_cache = hash_create("RI compare cache", RI_INIT_QUERYHASHSIZE, &ctl, HASH_ELEM | HASH_BLOBS); + + CacheRegisterSyscacheCallback(CASTSOURCETARGET, + InvalidateCastCacheCallBack, + (Datum) 0); } @@ -4184,7 +4230,7 @@ static bool ri_CompareWithCast(Oid eq_opr, Oid typeid, Oid collid, Datum lhs, Datum rhs) { - RI_CompareHashEntry *entry = ri_HashCompareOp(eq_opr, typeid); + RI_CompareInfo *entry = ri_HashCompareOp(eq_opr, typeid); /* Do we need to cast the values? */ if (OidIsValid(entry->cast_func_finfo.fn_oid)) @@ -4229,7 +4275,7 @@ ri_CompareWithCast(Oid eq_opr, Oid typeid, Oid collid, * its right-hand input, a cast function to coerce the value before * comparison. */ -static RI_CompareHashEntry * +static RI_CompareInfo * ri_HashCompareOp(Oid eq_opr, Oid typeid) { RI_CompareKey key; @@ -4252,23 +4298,20 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid) &key, HASH_ENTER, &found); if (!found) - entry->valid = false; + entry->info = NULL; /* - * If not already initialized, do so. Since we'll keep this hash entry - * for the life of the backend, put any subsidiary info for the function - * cache structs into TopMemoryContext. + * If not already initialized, build a new generation of call information. + * Use a separate context so invalidation cannot affect active callers. */ - if (!entry->valid) + if (entry->info == NULL) { Oid lefttype, righttype, castfunc; CoercionPathType pathtype; - - /* We always need to know how to call the equality operator */ - fmgr_info_cxt(get_opcode(eq_opr), &entry->eq_opr_finfo, - TopMemoryContext); + MemoryContext context; + RI_CompareInfo *info; /* * If we chose to use a cast from FK to PK type, we may have to apply @@ -4313,15 +4356,23 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid) format_type_be(lefttype)); } } + + /* Leave incomplete entries subject to normal error cleanup. */ + context = AllocSetContextCreate(CurTransactionContext, + "RI compare info", + ALLOCSET_SMALL_SIZES); + info = MemoryContextAllocZero(context, sizeof(RI_CompareInfo)); + info->context = context; + fmgr_info_cxt(get_opcode(eq_opr), &info->eq_opr_finfo, context); if (OidIsValid(castfunc)) - fmgr_info_cxt(castfunc, &entry->cast_func_finfo, - TopMemoryContext); + fmgr_info_cxt(castfunc, &info->cast_func_finfo, context); else - entry->cast_func_finfo.fn_oid = InvalidOid; - entry->valid = true; + info->cast_func_finfo.fn_oid = InvalidOid; + MemoryContextSetParent(context, TopMemoryContext); + entry->info = info; } - return entry; + return entry->info; } @@ -4519,18 +4570,25 @@ AtEOXact_RI(bool isCommit) ri_fastpath_flushing = false; /* - * Release fast-path metadata detached during this transaction by - * InvalidateConstraintCacheCallBack(). We are past every RI check that - * could still hold a pointer into one of these, so freeing here is safe - * on both the commit and the abort path. + * Release comparison and fast-path call information detached by + * invalidation callbacks. We are past every RI check that could still + * hold a pointer into one of these, so freeing here is safe on both the + * commit and the abort path. */ + while (ri_compare_dead_list != NULL) + { + RI_CompareInfo *dead = ri_compare_dead_list; + + ri_compare_dead_list = dead->next_dead; + MemoryContextDelete(dead->context); + } + while (ri_fpmeta_dead_list != NULL) { FastPathMeta *dead = ri_fpmeta_dead_list; ri_fpmeta_dead_list = dead->next_dead; MemoryContextDelete(dead->scratch_cxt); - pfree(dead); } } diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index c54a9895b55..d551f8531ac 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -1081,6 +1081,123 @@ CREATE TABLE PKTABLE (ptest1 int, ptest2 inet, ptest3 int, ptest4 inet, PRIMARY ptest3) REFERENCES pktable); ERROR: foreign key constraint "pktable_ptest4_ptest3_fkey" cannot be implemented DETAIL: Key columns "ptest4" of the referencing table and "ptest1" of the referenced table are of incompatible types: inet and integer. +-- Replacing a cast must invalidate cached RI comparison call information. +CREATE TYPE fk_cast_type AS (v int); +CREATE FUNCTION fk_cast1(fk_cast_type) RETURNS int + LANGUAGE sql IMMUTABLE STRICT AS 'SELECT $1.v'; +CREATE CAST (fk_cast_type AS int) WITH FUNCTION fk_cast1(fk_cast_type) AS IMPLICIT; +CREATE TABLE fk_cast_pk (id int PRIMARY KEY); +CREATE TABLE fk_cast_fk (label text PRIMARY KEY, id fk_cast_type REFERENCES fk_cast_pk); +INSERT INTO fk_cast_pk VALUES (1); +INSERT INTO fk_cast_fk VALUES ('original', ROW(1)::fk_cast_type); +-- With autocommit, this compares a committed row and commits the updated row. +-- Updating a row inserted in the same transaction would skip the comparison. +UPDATE fk_cast_fk SET id = id WHERE label = 'original'; +BEGIN; +SAVEPOINT original_cast; +DROP CAST (fk_cast_type AS int); +CREATE FUNCTION fk_cast2(fk_cast_type) RETURNS int + LANGUAGE sql IMMUTABLE STRICT AS 'SELECT $1.v'; +CREATE CAST (fk_cast_type AS int) WITH FUNCTION fk_cast2(fk_cast_type) AS IMPLICIT; +DROP FUNCTION fk_cast1(fk_cast_type); +-- Exercise the comparison cache before a new INSERT can rebuild other caches. +UPDATE fk_cast_fk SET id = id WHERE label = 'original'; +INSERT INTO fk_cast_fk VALUES ('replacement', ROW(1)::fk_cast_type); +SAVEPOINT invalid_key; +INSERT INTO fk_cast_fk VALUES ('invalid', ROW(2)::fk_cast_type); -- must fail +ERROR: insert or update on table "fk_cast_fk" violates foreign key constraint "fk_cast_fk_id_fkey" +DETAIL: Key (id)=((2)) is not present in table "fk_cast_pk". +ROLLBACK TO invalid_key; +-- Exercise restoration immediately, before any further DDL invalidates caches. +ROLLBACK TO original_cast; +UPDATE fk_cast_fk SET id = id WHERE label = 'original'; +INSERT INTO fk_cast_fk VALUES ('restored', ROW(1)::fk_cast_type); +SELECT count(*) FROM fk_cast_fk; + count +------- + 2 +(1 row) + +ROLLBACK; +-- Failed fast-path rebuilds must not accumulate metadata on subxact abort. +BEGIN; +SAVEPOINT missing_cast; +DROP CAST (fk_cast_type AS int); +DO $$ +DECLARE + before_count bigint; + after_count bigint; +BEGIN + SELECT count(*) INTO before_count FROM pg_backend_memory_contexts + WHERE name LIKE 'RI %'; + FOR i IN 1..10 LOOP + BEGIN + INSERT INTO fk_cast_fk VALUES ('failed', ROW(1)::fk_cast_type); + RAISE EXCEPTION 'missing cast was not detected'; + EXCEPTION WHEN internal_error THEN + IF SQLERRM NOT LIKE 'no conversion function%' THEN + RAISE; + END IF; + END; + END LOOP; + SELECT count(*) INTO after_count FROM pg_backend_memory_contexts + WHERE name LIKE 'RI %'; + IF after_count > before_count THEN + RAISE EXCEPTION 'RI contexts leaked across failed checks'; + END IF; +END $$; +ROLLBACK TO missing_cast; +INSERT INTO fk_cast_fk VALUES ('after_error', ROW(1)::fk_cast_type); +ROLLBACK; +DROP TABLE fk_cast_fk, fk_cast_pk; +DROP CAST (fk_cast_type AS int); +DROP FUNCTION fk_cast1(fk_cast_type); +DROP TYPE fk_cast_type; +-- Invalidation during a comparison must not overwrite its call information. +CREATE TYPE fk_cast_type AS (v int); +CREATE TABLE fk_cast_guard (armed bool); +CREATE FUNCTION fk_cast1(fk_cast_type) RETURNS int LANGUAGE plpgsql AS $$ +BEGIN + IF EXISTS (SELECT FROM fk_cast_guard) THEN + DELETE FROM fk_cast_guard; + EXECUTE 'DROP CAST (fk_cast_type AS bigint)'; + -- Compare a different committed row, rebuilding the same cache entry. + UPDATE fk_cast_fk SET id = id WHERE label = 'nested'; + END IF; + RETURN $1.v; +END $$; +CREATE FUNCTION fk_cast_aux(fk_cast_type) RETURNS bigint + LANGUAGE sql IMMUTABLE STRICT AS 'SELECT $1.v::bigint'; +CREATE CAST (fk_cast_type AS int) WITH FUNCTION fk_cast1(fk_cast_type) AS IMPLICIT; +CREATE CAST (fk_cast_type AS bigint) WITH FUNCTION fk_cast_aux(fk_cast_type); +CREATE TABLE fk_cast_pk (id int PRIMARY KEY); +CREATE TABLE fk_cast_fk (label text PRIMARY KEY, id fk_cast_type REFERENCES fk_cast_pk); +INSERT INTO fk_cast_pk VALUES (1); +INSERT INTO fk_cast_fk VALUES ('outer', ROW(1)::fk_cast_type), + ('nested', ROW(1)::fk_cast_type); +UPDATE fk_cast_fk SET id = id WHERE label = 'outer'; +BEGIN; +INSERT INTO fk_cast_guard VALUES (true); +-- Both the outer and nested UPDATE compare rows from earlier transactions. +UPDATE fk_cast_fk SET id = id WHERE label = 'outer'; +SELECT count(*) FROM fk_cast_guard; + count +------- + 0 +(1 row) + +SELECT count(*) FROM fk_cast_fk; + count +------- + 2 +(1 row) + +ROLLBACK; +DROP TABLE fk_cast_fk, fk_cast_pk, fk_cast_guard; +DROP CAST (fk_cast_type AS int); +DROP CAST (fk_cast_type AS bigint); +DROP FUNCTION fk_cast1(fk_cast_type), fk_cast_aux(fk_cast_type); +DROP TYPE fk_cast_type; -- -- Now some cases with inheritance -- Basic 2 table case: 1 column of matching types. diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index c013d1f8834..8986b93a5ed 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -747,6 +747,116 @@ ptest3) REFERENCES pktable(ptest1, ptest2)); CREATE TABLE PKTABLE (ptest1 int, ptest2 inet, ptest3 int, ptest4 inet, PRIMARY KEY(ptest1, ptest2), FOREIGN KEY(ptest4, ptest3) REFERENCES pktable); +-- Replacing a cast must invalidate cached RI comparison call information. +CREATE TYPE fk_cast_type AS (v int); +CREATE FUNCTION fk_cast1(fk_cast_type) RETURNS int + LANGUAGE sql IMMUTABLE STRICT AS 'SELECT $1.v'; +CREATE CAST (fk_cast_type AS int) WITH FUNCTION fk_cast1(fk_cast_type) AS IMPLICIT; +CREATE TABLE fk_cast_pk (id int PRIMARY KEY); +CREATE TABLE fk_cast_fk (label text PRIMARY KEY, id fk_cast_type REFERENCES fk_cast_pk); +INSERT INTO fk_cast_pk VALUES (1); +INSERT INTO fk_cast_fk VALUES ('original', ROW(1)::fk_cast_type); + +-- With autocommit, this compares a committed row and commits the updated row. +-- Updating a row inserted in the same transaction would skip the comparison. +UPDATE fk_cast_fk SET id = id WHERE label = 'original'; + +BEGIN; +SAVEPOINT original_cast; +DROP CAST (fk_cast_type AS int); +CREATE FUNCTION fk_cast2(fk_cast_type) RETURNS int + LANGUAGE sql IMMUTABLE STRICT AS 'SELECT $1.v'; +CREATE CAST (fk_cast_type AS int) WITH FUNCTION fk_cast2(fk_cast_type) AS IMPLICIT; +DROP FUNCTION fk_cast1(fk_cast_type); + +-- Exercise the comparison cache before a new INSERT can rebuild other caches. +UPDATE fk_cast_fk SET id = id WHERE label = 'original'; +INSERT INTO fk_cast_fk VALUES ('replacement', ROW(1)::fk_cast_type); +SAVEPOINT invalid_key; +INSERT INTO fk_cast_fk VALUES ('invalid', ROW(2)::fk_cast_type); -- must fail +ROLLBACK TO invalid_key; + +-- Exercise restoration immediately, before any further DDL invalidates caches. +ROLLBACK TO original_cast; +UPDATE fk_cast_fk SET id = id WHERE label = 'original'; +INSERT INTO fk_cast_fk VALUES ('restored', ROW(1)::fk_cast_type); +SELECT count(*) FROM fk_cast_fk; +ROLLBACK; + +-- Failed fast-path rebuilds must not accumulate metadata on subxact abort. +BEGIN; +SAVEPOINT missing_cast; +DROP CAST (fk_cast_type AS int); +DO $$ +DECLARE + before_count bigint; + after_count bigint; +BEGIN + SELECT count(*) INTO before_count FROM pg_backend_memory_contexts + WHERE name LIKE 'RI %'; + FOR i IN 1..10 LOOP + BEGIN + INSERT INTO fk_cast_fk VALUES ('failed', ROW(1)::fk_cast_type); + RAISE EXCEPTION 'missing cast was not detected'; + EXCEPTION WHEN internal_error THEN + IF SQLERRM NOT LIKE 'no conversion function%' THEN + RAISE; + END IF; + END; + END LOOP; + SELECT count(*) INTO after_count FROM pg_backend_memory_contexts + WHERE name LIKE 'RI %'; + IF after_count > before_count THEN + RAISE EXCEPTION 'RI contexts leaked across failed checks'; + END IF; +END $$; +ROLLBACK TO missing_cast; +INSERT INTO fk_cast_fk VALUES ('after_error', ROW(1)::fk_cast_type); +ROLLBACK; + +DROP TABLE fk_cast_fk, fk_cast_pk; +DROP CAST (fk_cast_type AS int); +DROP FUNCTION fk_cast1(fk_cast_type); +DROP TYPE fk_cast_type; + +-- Invalidation during a comparison must not overwrite its call information. +CREATE TYPE fk_cast_type AS (v int); +CREATE TABLE fk_cast_guard (armed bool); +CREATE FUNCTION fk_cast1(fk_cast_type) RETURNS int LANGUAGE plpgsql AS $$ +BEGIN + IF EXISTS (SELECT FROM fk_cast_guard) THEN + DELETE FROM fk_cast_guard; + EXECUTE 'DROP CAST (fk_cast_type AS bigint)'; + -- Compare a different committed row, rebuilding the same cache entry. + UPDATE fk_cast_fk SET id = id WHERE label = 'nested'; + END IF; + RETURN $1.v; +END $$; +CREATE FUNCTION fk_cast_aux(fk_cast_type) RETURNS bigint + LANGUAGE sql IMMUTABLE STRICT AS 'SELECT $1.v::bigint'; +CREATE CAST (fk_cast_type AS int) WITH FUNCTION fk_cast1(fk_cast_type) AS IMPLICIT; +CREATE CAST (fk_cast_type AS bigint) WITH FUNCTION fk_cast_aux(fk_cast_type); +CREATE TABLE fk_cast_pk (id int PRIMARY KEY); +CREATE TABLE fk_cast_fk (label text PRIMARY KEY, id fk_cast_type REFERENCES fk_cast_pk); +INSERT INTO fk_cast_pk VALUES (1); +INSERT INTO fk_cast_fk VALUES ('outer', ROW(1)::fk_cast_type), + ('nested', ROW(1)::fk_cast_type); +UPDATE fk_cast_fk SET id = id WHERE label = 'outer'; + +BEGIN; +INSERT INTO fk_cast_guard VALUES (true); +-- Both the outer and nested UPDATE compare rows from earlier transactions. +UPDATE fk_cast_fk SET id = id WHERE label = 'outer'; +SELECT count(*) FROM fk_cast_guard; +SELECT count(*) FROM fk_cast_fk; +ROLLBACK; + +DROP TABLE fk_cast_fk, fk_cast_pk, fk_cast_guard; +DROP CAST (fk_cast_type AS int); +DROP CAST (fk_cast_type AS bigint); +DROP FUNCTION fk_cast1(fk_cast_type), fk_cast_aux(fk_cast_type); +DROP TYPE fk_cast_type; + -- -- Now some cases with inheritance -- Basic 2 table case: 1 column of matching types. diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 7aedaafab90..5bc98422258 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2501,6 +2501,7 @@ RBTreeIterator REPARSE_JUNCTION_DATA_BUFFER RIX RI_CompareHashEntry +RI_CompareInfo RI_CompareKey RI_ConstraintInfo RI_FastPathEntry -- 2.47.3