From a272a58b94249879c3f7e170a5ebfaeec940d411 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Sat, 27 Jun 2026 09:07:07 +0200 Subject: [PATCH 01/36] Move FOR PORTION OF volatile check into planner This needs to be wary of the function volatility changing after we check it. We cannot enforce this when checking at parse time, so move it later, into the planner. Author: Paul A. Jungwirth Reported-by: Tom Lane Reviewed-by: jian he Discussion: https://www.postgresql.org/message-id/flat/CA%2BrenyUte0_UJsJiDJQi82oaBsMJn%3Dcct0Wn%3DvOqXtuDn%3DYYJA%40mail.gmail.com --- src/backend/optimizer/plan/planner.c | 12 ++++++++++++ src/backend/parser/analyze.c | 3 --- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index f4689e7c9f8bf..846bd7c1fbe6a 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -1081,6 +1081,18 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name, /* exclRelTlist contains only Vars, so no preprocessing needed */ } + if (parse->forPortionOf) + { + parse->forPortionOf->targetRange = + preprocess_expression(root, + parse->forPortionOf->targetRange, + EXPRKIND_TARGET); + if (contain_volatile_functions(parse->forPortionOf->targetRange)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("FOR PORTION OF bounds cannot contain volatile functions"))); + } + foreach(l, parse->mergeActionList) { MergeAction *action = (MergeAction *) lfirst(l); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 93fa66ae57ce7..76758adefb6f1 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1489,9 +1489,6 @@ transformForPortionOfClause(ParseState *pstate, args, InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); } - if (contain_volatile_functions_after_planning((Expr *) result->targetRange)) - ereport(ERROR, - (errmsg("FOR PORTION OF bounds cannot contain volatile functions"))); /* * Build overlapsExpr to use as an extra qual. This means we only hit rows From a40fdf658862b3221a35268f8c74abfd46b9e93c Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Sat, 27 Jun 2026 19:34:40 +0200 Subject: [PATCH 02/36] Reject child partition FDWs in FOR PORTION OF We should defer validating FDW usage until after analysis. We have to guard against not just the topmost table, but also individual child partitions. Added the check to CheckValidResultRel, because it is called after looking up child partitions (accounting for pruning), but before the FDW can run a DirectModify update, which would bypass per-tuple executor work. Author: jian he Reported-by: Tom Lane Reviewed-by: Paul A. Jungwirth Discussion: https://www.postgresql.org/message-id/flat/CA%2BrenyUte0_UJsJiDJQi82oaBsMJn%3Dcct0Wn%3DvOqXtuDn%3DYYJA%40mail.gmail.com --- .../postgres_fdw/expected/postgres_fdw.out | 42 +++++++++++++++++-- contrib/postgres_fdw/sql/postgres_fdw.sql | 30 +++++++++++-- src/backend/commands/copyfrom.c | 2 +- src/backend/executor/execMain.c | 11 ++++- src/backend/executor/execPartition.c | 4 +- src/backend/executor/nodeModifyTable.c | 2 +- src/backend/parser/analyze.c | 6 --- src/include/executor/executor.h | 2 +- 8 files changed, 81 insertions(+), 18 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 13853b8b72079..0805c56cb1ba5 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -6335,9 +6335,10 @@ DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass; -- Test UPDATE FOR PORTION OF UPDATE ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01' -SET c2 = c2 + 1 -WHERE c1 = '[1,2)'; + SET c2 = c2 + 1 + WHERE c1 = '[1,2)'; -- error ERROR: foreign tables don't support FOR PORTION OF +DETAIL: "ft8" is a foreign table. SELECT * FROM ft8 WHERE c1 = '[1,2)' ORDER BY c1, c4; c1 | c2 | c3 | c4 -------+----+--------+------------------------- @@ -6346,14 +6347,49 @@ SELECT * FROM ft8 WHERE c1 = '[1,2)' ORDER BY c1, c4; -- Test DELETE FOR PORTION OF DELETE FROM ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01' -WHERE c1 = '[2,3)'; + WHERE c1 = '[2,3)'; -- error ERROR: foreign tables don't support FOR PORTION OF +DETAIL: "ft8" is a foreign table. SELECT * FROM ft8 WHERE c1 = '[2,3)' ORDER BY c1, c4; c1 | c2 | c3 | c4 -------+----+--------+------------------------- [2,3) | 3 | AAA002 | [01-01-2000,01-01-2020) (1 row) +-- FOR PORTION OF fails if a child partition is a foreign table, even if the +-- root is not. But a child partition that is pruned doesn't cause an error. +CREATE TABLE fpo_part_parent ( + c1 int4range NOT NULL, + c2 int NOT NULL, + c3 text, + c4 daterange NOT NULL +) PARTITION BY LIST (c2); +CREATE TABLE fpo_part_local PARTITION OF fpo_part_parent FOR VALUES IN (1); +INSERT INTO fpo_part_local VALUES ('[1,2)', 1, 'one', '[2024-01-01,2024-12-31)'); +CREATE FOREIGN TABLE fpo_part_foreign + PARTITION OF fpo_part_parent FOR VALUES IN (6) + SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 5'); +DELETE FROM fpo_part_parent + FOR PORTION OF c4 FROM '2001-01-01' TO '2001-02-01' WHERE c2 = 6; -- error +ERROR: foreign tables don't support FOR PORTION OF +DETAIL: "fpo_part_foreign" is a foreign table. +UPDATE fpo_part_parent + FOR PORTION OF c4 FROM '2001-01-01' TO '2001-02-01' SET c3 = 'x' WHERE c2 = 6; -- error +ERROR: foreign tables don't support FOR PORTION OF +DETAIL: "fpo_part_foreign" is a foreign table. +UPDATE fpo_part_parent + FOR PORTION OF c4 FROM '2024-06-01' TO '2024-07-01' SET c3 = 'edited' WHERE c2 = 1; -- okay +DELETE FROM fpo_part_parent + FOR PORTION OF c4 FROM '2024-06-01' TO '2024-06-15' WHERE c2 = 1; -- okay +SELECT c1, c2, c3, c4 FROM fpo_part_local ORDER BY c4; + c1 | c2 | c3 | c4 +-------+----+--------+------------------------- + [1,2) | 1 | one | [01-01-2024,06-01-2024) + [1,2) | 1 | edited | [06-15-2024,07-01-2024) + [1,2) | 1 | one | [07-01-2024,12-31-2024) +(3 rows) + +DROP TABLE fpo_part_parent; -- Test UPDATE/DELETE with RETURNING on a three-table join INSERT INTO ft2 (c1,c2,c3) SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id; diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 697c4a92e2d03..8162c5496bfd2 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -1578,15 +1578,39 @@ DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass; -- Test UPDATE FOR PORTION OF UPDATE ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01' -SET c2 = c2 + 1 -WHERE c1 = '[1,2)'; + SET c2 = c2 + 1 + WHERE c1 = '[1,2)'; -- error SELECT * FROM ft8 WHERE c1 = '[1,2)' ORDER BY c1, c4; -- Test DELETE FOR PORTION OF DELETE FROM ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01' -WHERE c1 = '[2,3)'; + WHERE c1 = '[2,3)'; -- error SELECT * FROM ft8 WHERE c1 = '[2,3)' ORDER BY c1, c4; +-- FOR PORTION OF fails if a child partition is a foreign table, even if the +-- root is not. But a child partition that is pruned doesn't cause an error. +CREATE TABLE fpo_part_parent ( + c1 int4range NOT NULL, + c2 int NOT NULL, + c3 text, + c4 daterange NOT NULL +) PARTITION BY LIST (c2); +CREATE TABLE fpo_part_local PARTITION OF fpo_part_parent FOR VALUES IN (1); +INSERT INTO fpo_part_local VALUES ('[1,2)', 1, 'one', '[2024-01-01,2024-12-31)'); +CREATE FOREIGN TABLE fpo_part_foreign + PARTITION OF fpo_part_parent FOR VALUES IN (6) + SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 5'); +DELETE FROM fpo_part_parent + FOR PORTION OF c4 FROM '2001-01-01' TO '2001-02-01' WHERE c2 = 6; -- error +UPDATE fpo_part_parent + FOR PORTION OF c4 FROM '2001-01-01' TO '2001-02-01' SET c3 = 'x' WHERE c2 = 6; -- error +UPDATE fpo_part_parent + FOR PORTION OF c4 FROM '2024-06-01' TO '2024-07-01' SET c3 = 'edited' WHERE c2 = 1; -- okay +DELETE FROM fpo_part_parent + FOR PORTION OF c4 FROM '2024-06-01' TO '2024-06-15' WHERE c2 = 1; -- okay +SELECT c1, c2, c3, c4 FROM fpo_part_local ORDER BY c4; +DROP TABLE fpo_part_parent; + -- Test UPDATE/DELETE with RETURNING on a three-table join INSERT INTO ft2 (c1,c2,c3) SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id; diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c index 0087585b2c4e1..80a527ed4c642 100644 --- a/src/backend/commands/copyfrom.c +++ b/src/backend/commands/copyfrom.c @@ -921,7 +921,7 @@ CopyFrom(CopyFromState cstate) ExecInitResultRelation(estate, resultRelInfo, 1); /* Verify the named relation is a valid target for INSERT */ - CheckValidResultRel(resultRelInfo, CMD_INSERT, ONCONFLICT_NONE, NIL); + CheckValidResultRel(resultRelInfo, CMD_INSERT, ONCONFLICT_NONE, NIL, NULL); ExecOpenIndices(resultRelInfo, false); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 4b30f7686801a..c3af96989ba3e 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -1063,7 +1063,8 @@ InitPlan(QueryDesc *queryDesc, int eflags) */ void CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation, - OnConflictAction onConflictAction, List *mergeActions) + OnConflictAction onConflictAction, List *mergeActions, + ModifyTable *mtnode) { Relation resultRel = resultRelInfo->ri_RelationDesc; FdwRoutine *fdwroutine; @@ -1126,6 +1127,14 @@ CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation, RelationGetRelationName(resultRel)))); break; case RELKIND_FOREIGN_TABLE: + /* We don't support FOR PORTION OF FDW queries. */ + if (mtnode && mtnode->forPortionOf) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("foreign tables don't support FOR PORTION OF"), + errdetail("\"%s\" is a foreign table.", + RelationGetRelationName(resultRel))); + /* Okay only if the FDW supports it */ fdwroutine = resultRelInfo->ri_FdwRoutine; switch (operation) diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c index d96d4f9947b79..33ec5bfde4c4d 100644 --- a/src/backend/executor/execPartition.c +++ b/src/backend/executor/execPartition.c @@ -368,7 +368,7 @@ ExecFindPartition(ModifyTableState *mtstate, /* Verify this ResultRelInfo allows INSERTs */ CheckValidResultRel(rri, CMD_INSERT, node ? node->onConflictAction : ONCONFLICT_NONE, - NIL); + NIL, node); /* * Initialize information needed to insert this and @@ -594,7 +594,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate, * required when the operation is CMD_UPDATE. */ CheckValidResultRel(leaf_part_rri, CMD_INSERT, - node ? node->onConflictAction : ONCONFLICT_NONE, NIL); + node ? node->onConflictAction : ONCONFLICT_NONE, NIL, node); /* * Open partition indices. The user may have asked to check for conflicts diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 846dc516b4366..c333d7139fae0 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -5312,7 +5312,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) * Verify result relation is a valid target for the current operation */ CheckValidResultRel(resultRelInfo, operation, node->onConflictAction, - mergeActions); + mergeActions, node); resultRelInfo++; i++; diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 76758adefb6f1..dc65a505c1657 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1335,12 +1335,6 @@ transformForPortionOfClause(ParseState *pstate, ForPortionOfExpr *result; Var *rangeVar; - /* We don't support FOR PORTION OF FDW queries. */ - if (targetrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("foreign tables don't support FOR PORTION OF"))); - result = makeNode(ForPortionOfExpr); /* Look up the FOR PORTION OF name requested. */ diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 650baab3efcaa..1798e6027d4c2 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -249,7 +249,7 @@ extern bool ExecCheckPermissions(List *rangeTable, extern bool ExecCheckOneRelPerms(RTEPermissionInfo *perminfo); extern void CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation, OnConflictAction onConflictAction, - List *mergeActions); + List *mergeActions, ModifyTable *mtnode); extern void InitResultRelInfo(ResultRelInfo *resultRelInfo, Relation resultRelationDesc, Index resultRelationIndex, From effb923d9dec8fd4a5102fee80e52d65d86747c8 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Sat, 27 Jun 2026 16:48:35 -0400 Subject: [PATCH 03/36] COPY TO FORMAT JSON: respect column list order When COPY TO with FORMAT json is given an explicit column list that names all columns in a different order, the JSON output incorrectly used the table's physical column order instead of the requested order. This happened because BeginCopyTo() only built a restricted TupleDesc when list_length(attnumlist) < tupDesc->natts. When all columns are listed (just reordered), this condition was false and no projected TupleDesc was built, causing CopyToJsonOneRow() to emit columns in physical order. Fix by also building the projected TupleDesc when an explicit column list was provided (attnamelist != NIL), even if it names all columns. Author: Baji Shaik Reviewed-by: Andrew Dunstan Discussion: https://postgr.es/m/CA+fm-ROd4cNKM524n6EdgtZ9xOzOHJDNv8J_9Mvr2+2t1qWSDw@mail.gmail.com --- src/backend/commands/copyto.c | 14 +++++++++----- src/test/regress/expected/copy.out | 9 +++++++++ src/test/regress/sql/copy.sql | 9 +++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index 6755bb698de73..d30cad019c739 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -1051,15 +1051,19 @@ BeginCopyTo(ParseState *pstate, { cstate->json_buf = makeStringInfo(); - if (rel && list_length(cstate->attnumlist) < tupDesc->natts) + /* + * Build a projected TupleDesc describing only the selected columns + * so that composite_to_json() emits the right column names and + * types; needed when an explicit column list was given (possibly + * with a different column order) or when generated columns are + * excluded from the output. + */ + if (rel && (attnamelist != NIL || + list_length(cstate->attnumlist) < tupDesc->natts)) { int natts = list_length(cstate->attnumlist); TupleDesc resultDesc; - /* - * Build a TupleDesc describing only the selected columns so that - * composite_to_json() emits the right column names and types. - */ resultDesc = CreateTemplateTupleDesc(natts); foreach_int(attnum, cstate->attnumlist) diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out index 37498cdd6e7aa..ace38225623af 100644 --- a/src/test/regress/expected/copy.out +++ b/src/test/regress/expected/copy.out @@ -73,6 +73,15 @@ copy copytest3 to stdout csv header; c1,"col with , comma","col with "" quote" 1,a,1 2,b,2 +-- testing explicit column order +create temp table copytest_order (a int, b int, c int); +copy copytest_order from stdin; +copy copytest_order (c, b, a) to stdout; +3 2 1 +copy copytest_order (c, b, a) to stdout (format csv); +3,2,1 +copy copytest_order (c, b, a) to stdout (format json); +{"c":3,"b":2,"a":1} --- test copying in JSON mode with various styles copy (select 1 union all select 2) to stdout with (format json); {"?column?":1} diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql index 094fd76c12bfb..507b822946fb2 100644 --- a/src/test/regress/sql/copy.sql +++ b/src/test/regress/sql/copy.sql @@ -82,6 +82,15 @@ this is just a line full of junk that would error out if parsed copy copytest3 to stdout csv header; +-- testing explicit column order +create temp table copytest_order (a int, b int, c int); +copy copytest_order from stdin; +1 2 3 +\. +copy copytest_order (c, b, a) to stdout; +copy copytest_order (c, b, a) to stdout (format csv); +copy copytest_order (c, b, a) to stdout (format json); + --- test copying in JSON mode with various styles copy (select 1 union all select 2) to stdout with (format json); copy (select 1 as foo union all select 2) to stdout with (format json); From 0cd17fdd3c000f6e64e79da0fea5e65dc9f562df Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Sat, 27 Jun 2026 17:42:51 -0400 Subject: [PATCH 04/36] Prevent inherited CHECK constraints from being weakened MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disallow marking an inherited CHECK constraint as NOT ENFORCED when an equivalent parent constraint remains ENFORCED. This prevents ALTER CONSTRAINT from producing a child constraint that is weaker than one of its inherited parent definitions. When recursively altering a CHECK constraint to NOT ENFORCED, collect the corresponding constraints in the affected inheritance subtree and ignore those parent constraints while checking descendants. If a descendant also inherits an equivalent ENFORCED constraint from a parent outside the current ALTER, keep the descendant ENFORCED by merging to the stricter state. This was missed in commit 342051d73b3, which introduced the ability to alter CHECK constraint enforceability. Add regression coverage for direct child ALTER, ONLY ALTER, mixed-parent inheritance, and a common-ancestor diamond where all equivalent inherited constraints can be changed together. Author: Chao Li Reviewed-by: Jian He Reviewed-by: Zsolt Parragi Reviewed-by: Álvaro Herrera Discussion: https://postgr.es/m/E74C57FA-1DD0-4C8E-8FB1-538034752592@gmail.com --- src/backend/commands/tablecmds.c | 256 ++++++++++++++++++++-- src/test/regress/expected/constraints.out | 15 +- src/test/regress/expected/inherit.out | 92 ++++++++ src/test/regress/sql/constraints.sql | 5 +- src/test/regress/sql/inherit.sql | 52 +++++ 5 files changed, 395 insertions(+), 25 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 33e065d61ce00..472db112fa7a0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -437,6 +437,7 @@ static bool ATExecAlterFKConstrEnforceability(List **wqueue, ATAlterConstraint * static bool ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, HeapTuple contuple, bool recurse, bool recursing, + List *changing_conids, LOCKMODE lockmode); static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Relation tgrel, Relation rel, @@ -459,6 +460,7 @@ static void AlterFKConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint static void AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Oid conrelid, bool recurse, bool recursing, + List *changing_conids, LOCKMODE lockmode); static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Relation tgrel, Relation rel, @@ -466,6 +468,10 @@ static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cm List **otherrelids, LOCKMODE lockmode); static void AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation conrel, HeapTuple contuple); +static bool ATCheckCheckConstrHasEnforcedParent(Relation conrel, Relation rel, + HeapTuple contuple, + List *changing_conids, + Oid *enforced_parentoid); static ObjectAddress ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, bool recurse, bool recursing, LOCKMODE lockmode); @@ -477,6 +483,7 @@ static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relat static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel, HeapTuple contuple, bool recurse, bool recursing, LOCKMODE lockmode); +static bool constraints_equivalent(HeapTuple a, HeapTuple b, TupleDesc tupleDesc); static int transformColumnNameList(Oid relId, List *colList, int16 *attnums, Oid *atttypids, Oid *attcollids); static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, @@ -12459,7 +12466,7 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, else if (currcon->contype == CONSTRAINT_CHECK) changed = ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel, contuple, recurse, false, - lockmode); + NIL, lockmode); } else if (cmdcon->alterDeferrability && ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel, @@ -12646,12 +12653,16 @@ ATExecAlterFKConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, static bool ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, HeapTuple contuple, - bool recurse, bool recursing, LOCKMODE lockmode) + bool recurse, bool recursing, + List *changing_conids, + LOCKMODE lockmode) { Form_pg_constraint currcon; Relation rel; bool changed = false; List *children = NIL; + bool target_enforced = cmdcon->is_enforced; + Oid enforced_parentoid = InvalidOid; /* Since this function recurses, it could be driven to stack overflow */ check_stack_depth(); @@ -12668,16 +12679,57 @@ ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, */ rel = table_open(currcon->conrelid, NoLock); - if (currcon->conenforced != cmdcon->is_enforced) + /* + * When setting a constraint to NOT ENFORCED, check whether any matching + * parent constraint remains ENFORCED and is not part of this ALTER. + * + * For a direct ALTER of an inherited constraint, reject the command, + * because the child cannot be weakened while its parent remains enforced. + * + * During recursion, another parent outside this ALTER may still enforce + * the same constraint in a regular inheritance hierarchy. In that case, + * keep the child constraint ENFORCED so that its merged enforceability + * still reflects the remaining enforced parent. Partitions do not need + * this recursive parent check because a partition can have only one + * direct parent. + */ + if (!cmdcon->is_enforced && + (!recursing || !rel->rd_rel->relispartition) && + ATCheckCheckConstrHasEnforcedParent(conrel, rel, contuple, + changing_conids, + &enforced_parentoid)) { - AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); + if (!recursing) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot mark inherited constraint \"%s\" as %s", + NameStr(currcon->conname), + "NOT ENFORCED"), + errdetail("The matching constraint on parent table \"%s\" is %s.", + get_rel_name(enforced_parentoid), "ENFORCED")); + + target_enforced = true; + } + + /* + * Update to the merged enforceability if needed. This may differ from the + * requested enforceability when another matching parent constraint + * remains enforced. + */ + if (currcon->conenforced != target_enforced) + { + ATAlterConstraint updatecon = *cmdcon; + + updatecon.is_enforced = target_enforced; + AlterConstrUpdateConstraintEntry(&updatecon, conrel, contuple); changed = true; } /* * Note that we must recurse even when trying to change a check constraint * to not enforced if it is already not enforced, in case descendant - * constraints might be enforced and need to be changed to not enforced. + * constraints might be enforced and need to be changed to not enforced, + * unless they still inherit an enforced constraint from another parent. * Conversely, we should do nothing if a constraint is being set to * enforced and is already enforced, as descendant constraints cannot be * different in that case. @@ -12690,28 +12742,66 @@ ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, * try to look for it in the children. */ if (!recursing && !currcon->connoinherit) + { + Assert(changing_conids == NIL); + children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); + /* + * When setting NOT ENFORCED, build the set of equivalent CHECK + * constraints that this command will attempt to change before + * visiting descendants. The root itself has already been checked + * above. + */ + if (!cmdcon->is_enforced) + changing_conids = list_make1_oid(currcon->oid); + + foreach_oid(childoid, children) + { + if (childoid == RelationGetRelid(rel)) + continue; + + /* + * If we are told not to recurse, there had better not be any + * child tables, because we can't change constraint + * enforceability on the parent unless we have changed + * enforceability for all child. + */ + if (!recurse) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be altered on child tables too"), + errhint("Do not specify the ONLY keyword.")); + + /* + * It is sufficient to look up the constraint by name here. + * Supported DDL ensures that inheritable CHECK constraints + * with the same name have equivalent definitions when they + * are propagated to children or when inheritance is + * established. All descendants returned by + * find_all_inheritors must have this constraint: inherited + * CHECK constraints propagate to all children at + * inheritance-link creation time and cannot be dropped + * independently on child tables. + */ + if (!cmdcon->is_enforced) + changing_conids = + list_append_unique_oid(changing_conids, + get_relation_constraint_oid(childoid, + cmdcon->conname, + false)); + } + } + foreach_oid(childoid, children) { if (childoid == RelationGetRelid(rel)) continue; - /* - * If we are told not to recurse, there had better not be any - * child tables, because we can't change constraint enforceability - * on the parent unless we have changed enforceability for all - * child. - */ - if (!recurse) - ereport(ERROR, - errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("constraint must be altered on child tables too"), - errhint("Do not specify the ONLY keyword.")); - AlterCheckConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, childoid, false, true, + changing_conids, lockmode); } } @@ -12723,7 +12813,7 @@ ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, */ if (rel->rd_rel->relkind == RELKIND_RELATION && !currcon->conenforced && - cmdcon->is_enforced) + target_enforced) { AlteredTableInfo *tab; NewConstraint *newcon; @@ -12763,6 +12853,7 @@ static void AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Oid conrelid, bool recurse, bool recursing, + List *changing_conids, LOCKMODE lockmode) { SysScanDesc pscan; @@ -12792,11 +12883,138 @@ AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, cmdcon->conname, get_rel_name(conrelid))); ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel, childtup, - recurse, recursing, lockmode); + recurse, recursing, changing_conids, + lockmode); systable_endscan(pscan); } +/* + * When setting an inherited CHECK constraint to NOT ENFORCED, look for a + * matching parent constraint that remains ENFORCED and is not part of the same + * ALTER. + */ +static bool +ATCheckCheckConstrHasEnforcedParent(Relation conrel, Relation rel, + HeapTuple contuple, + List *changing_conids, + Oid *enforced_parentoid) +{ + Form_pg_constraint currcon; + Relation inhrel; + SysScanDesc scan; + ScanKeyData skey; + HeapTuple inheritsTuple; + + /* Since this function recurses, it could be driven to stack overflow */ + check_stack_depth(); + + currcon = (Form_pg_constraint) GETSTRUCT(contuple); + Assert(currcon->contype == CONSTRAINT_CHECK); + + if (currcon->coninhcount <= 0) + return false; + + inhrel = table_open(InheritsRelationId, AccessShareLock); + + ScanKeyInit(&skey, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + scan = systable_beginscan(inhrel, InheritsRelidSeqnoIndexId, + true, NULL, 1, &skey); + + while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) + { + Oid parentoid; + Relation parentrel = NULL; + SysScanDesc pscan; + ScanKeyData pkey[3]; + HeapTuple parenttup; + + parentoid = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhparent; + + ScanKeyInit(&pkey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(parentoid)); + ScanKeyInit(&pkey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&pkey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + NameGetDatum(&currcon->conname)); + + pscan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, + true, NULL, 3, pkey); + + /* + * ConstraintRelidTypidNameIndexId is unique on (conrelid, contypid, + * conname), so this loop body executes at most once per parent. + */ + while (HeapTupleIsValid(parenttup = systable_getnext(pscan))) + { + Form_pg_constraint parentcon; + + parentcon = (Form_pg_constraint) GETSTRUCT(parenttup); + + if (parentcon->contype != CONSTRAINT_CHECK || + parentcon->connoinherit || + !parentcon->conenforced) + continue; + + if (!constraints_equivalent(parenttup, contuple, + RelationGetDescr(conrel))) + elog(ERROR, "child table \"%s\" has different definition for check constraint \"%s\"", + RelationGetRelationName(rel), + NameStr(parentcon->conname)); + + /* + * A parent listed in changing_conids is being changed by the same + * ALTER, but it may not have been updated yet. For regular + * inheritance, recurse upward to check whether an equivalent + * enforced parent outside the ALTER will make it remain enforced. + * Partitions cannot have multiple parents, so they do not need + * this check. + */ + if (!rel->rd_rel->relispartition && + list_member_oid(changing_conids, parentcon->oid)) + { + Oid parent_enforced_parentoid = InvalidOid; + + if (parentrel == NULL) + parentrel = table_open(parentoid, NoLock); + + if (!ATCheckCheckConstrHasEnforcedParent(conrel, + parentrel, + parenttup, + changing_conids, + &parent_enforced_parentoid)) + continue; + } + + *enforced_parentoid = parentoid; + if (parentrel != NULL) + table_close(parentrel, NoLock); + systable_endscan(pscan); + systable_endscan(scan); + table_close(inhrel, AccessShareLock); + return true; + } + + if (parentrel != NULL) + table_close(parentrel, NoLock); + systable_endscan(pscan); + } + + systable_endscan(scan); + table_close(inhrel, AccessShareLock); + + return false; +} + /* * Returns true if the constraint's deferrability is altered. * diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index e54fec7fb5777..83f97f684d542 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -446,8 +446,15 @@ alter table parted_ch_2 alter constraint cc_2 enforced; --error ERROR: check constraint "cc_2" of relation "parted_ch_2" is violated by some row delete from parted_ch where a = 16; alter table parted_ch_2 alter constraint cc_2 enforced; -alter table parted_ch_2 alter constraint cc not enforced; -alter table parted_ch_2 alter constraint cc_1 not enforced; +alter table parted_ch_2 alter constraint cc not enforced; --error +ERROR: cannot mark inherited constraint "cc" as NOT ENFORCED +DETAIL: The matching constraint on parent table "parted_ch" is ENFORCED. +alter table only parted_ch_2 alter constraint cc not enforced; --error +ERROR: cannot mark inherited constraint "cc" as NOT ENFORCED +DETAIL: The matching constraint on parent table "parted_ch" is ENFORCED. +alter table parted_ch_2 alter constraint cc_1 not enforced; --error +ERROR: cannot mark inherited constraint "cc_1" as NOT ENFORCED +DETAIL: The matching constraint on parent table "parted_ch" is ENFORCED. alter table parted_ch_2 alter constraint cc_2 not enforced; --check these CHECK constraint status again select * from check_constraint_status; @@ -457,12 +464,12 @@ select * from check_constraint_status; cc | parted_ch_1 | t | t cc | parted_ch_11 | t | t cc | parted_ch_12 | t | t - cc | parted_ch_2 | f | f + cc | parted_ch_2 | t | t cc_1 | parted_ch | t | t cc_1 | parted_ch_1 | t | t cc_1 | parted_ch_11 | t | t cc_1 | parted_ch_12 | t | t - cc_1 | parted_ch_2 | f | f + cc_1 | parted_ch_2 | t | t cc_2 | parted_ch_2 | f | f (11 rows) diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out index 3d8e8d8afd241..3c2ff55d3baa9 100644 --- a/src/test/regress/expected/inherit.out +++ b/src/test/regress/expected/inherit.out @@ -1479,6 +1479,98 @@ NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table p1_c1 drop cascades to table p1_c2 drop cascades to table p1_c3 +-- an inherited CHECK constraint cannot be NOT ENFORCED under an ENFORCED parent +create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced); +create table p1_c1() inherits(p1); +alter table p1_c1 alter constraint p1_a_check not enforced; --error +ERROR: cannot mark inherited constraint "p1_a_check" as NOT ENFORCED +DETAIL: The matching constraint on parent table "p1" is ENFORCED. +alter table p1 alter constraint p1_a_check not enforced; --ok +alter table p1_c1 alter constraint p1_a_check not enforced; --ok +drop table p1 cascade; +NOTICE: drop cascades to table p1_c1 +-- recursive NOT ENFORCED merges with ENFORCED constraints from other parents +create table p1(a int constraint p1_a_check check (a > 0) enforced); +create table p2(a int constraint p1_a_check check (a > 0) enforced); +create table p1_c1() inherits (p1, p2); +NOTICE: merging multiple inherited definitions of column "a" +-- make p1_c1_g2's oid smaller than p1_c1_g1's, to ensure ordering of +-- pg_constraint rows to not impact the test results. +create table p1_c1_g2() inherits (p1_c1); +create table p1_c1_g1() inherits (p1_c1); +alter table p1_c1_g2 inherit p1_c1_g1; +create table p1_c2() inherits (p1); +alter table p1 alter constraint p1_a_check not enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'p1_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; + conname | conenforced | convalidated | conrelid +------------+-------------+--------------+---------- + p1_a_check | f | f | p1 + p1_a_check | t | t | p1_c1 + p1_a_check | t | t | p1_c1_g1 + p1_a_check | t | t | p1_c1_g2 + p1_a_check | f | f | p1_c2 + p1_a_check | t | t | p2 +(6 rows) + +alter table p1_c1 alter constraint p1_a_check not enforced; --error +ERROR: cannot mark inherited constraint "p1_a_check" as NOT ENFORCED +DETAIL: The matching constraint on parent table "p2" is ENFORCED. +alter table p2 alter constraint p1_a_check not enforced; --ok +alter table p1_c1 alter constraint p1_a_check not enforced; --ok +drop table p1, p2 cascade; +NOTICE: drop cascades to 4 other objects +DETAIL: drop cascades to table p1_c1 +drop cascades to table p1_c1_g1 +drop cascades to table p1_c1_g2 +drop cascades to table p1_c2 +-- recursive NOT ENFORCED can change all matching enforced parents together +create table gp(a int constraint gp_a_check check (a > 0) enforced); +create table p1() inherits (gp); +create table p2() inherits (gp); +create table p1_c1() inherits (p1, p2); +NOTICE: merging multiple inherited definitions of column "a" +alter table gp alter constraint gp_a_check not enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'gp_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; + conname | conenforced | convalidated | conrelid +------------+-------------+--------------+---------- + gp_a_check | f | f | gp + gp_a_check | f | f | p1 + gp_a_check | f | f | p1_c1 + gp_a_check | f | f | p2 +(4 rows) + +drop table gp cascade; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table p1 +drop cascades to table p2 +drop cascades to table p1_c1 +-- recursive NOT ENFORCED can change a direct-plus-indirect diamond together +create table gp(a int constraint gp_a_check check (a > 0) enforced); +create table p1_c1() inherits (gp); +create table p1() inherits (gp); +alter table p1_c1 inherit p1; +alter table gp alter constraint gp_a_check not enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'gp_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; + conname | conenforced | convalidated | conrelid +------------+-------------+--------------+---------- + gp_a_check | f | f | gp + gp_a_check | f | f | p1 + gp_a_check | f | f | p1_c1 +(3 rows) + +drop table gp cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table p1 +drop cascades to table p1_c1 --for "no inherit" check constraint, it will not recurse to child table create table p1(f1 int constraint p1_a_check check (f1 > 0) no inherit not enforced); create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced); diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index dc133b124bbfd..9705962eb9f0e 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -309,8 +309,9 @@ select * from check_constraint_status; alter table parted_ch_2 alter constraint cc_2 enforced; --error delete from parted_ch where a = 16; alter table parted_ch_2 alter constraint cc_2 enforced; -alter table parted_ch_2 alter constraint cc not enforced; -alter table parted_ch_2 alter constraint cc_1 not enforced; +alter table parted_ch_2 alter constraint cc not enforced; --error +alter table only parted_ch_2 alter constraint cc not enforced; --error +alter table parted_ch_2 alter constraint cc_1 not enforced; --error alter table parted_ch_2 alter constraint cc_2 not enforced; --check these CHECK constraint status again diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql index 8f986904389c5..072fca13c1351 100644 --- a/src/test/regress/sql/inherit.sql +++ b/src/test/regress/sql/inherit.sql @@ -535,6 +535,58 @@ where conname = 'inh_check_constraint3' and contype = 'c' order by conrelid::regclass::text collate "C"; drop table p1 cascade; +-- an inherited CHECK constraint cannot be NOT ENFORCED under an ENFORCED parent +create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced); +create table p1_c1() inherits(p1); +alter table p1_c1 alter constraint p1_a_check not enforced; --error +alter table p1 alter constraint p1_a_check not enforced; --ok +alter table p1_c1 alter constraint p1_a_check not enforced; --ok +drop table p1 cascade; + +-- recursive NOT ENFORCED merges with ENFORCED constraints from other parents +create table p1(a int constraint p1_a_check check (a > 0) enforced); +create table p2(a int constraint p1_a_check check (a > 0) enforced); +create table p1_c1() inherits (p1, p2); +-- make p1_c1_g2's oid smaller than p1_c1_g1's, to ensure ordering of +-- pg_constraint rows to not impact the test results. +create table p1_c1_g2() inherits (p1_c1); +create table p1_c1_g1() inherits (p1_c1); +alter table p1_c1_g2 inherit p1_c1_g1; +create table p1_c2() inherits (p1); +alter table p1 alter constraint p1_a_check not enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'p1_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; +alter table p1_c1 alter constraint p1_a_check not enforced; --error +alter table p2 alter constraint p1_a_check not enforced; --ok +alter table p1_c1 alter constraint p1_a_check not enforced; --ok +drop table p1, p2 cascade; + +-- recursive NOT ENFORCED can change all matching enforced parents together +create table gp(a int constraint gp_a_check check (a > 0) enforced); +create table p1() inherits (gp); +create table p2() inherits (gp); +create table p1_c1() inherits (p1, p2); +alter table gp alter constraint gp_a_check not enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'gp_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; +drop table gp cascade; + +-- recursive NOT ENFORCED can change a direct-plus-indirect diamond together +create table gp(a int constraint gp_a_check check (a > 0) enforced); +create table p1_c1() inherits (gp); +create table p1() inherits (gp); +alter table p1_c1 inherit p1; +alter table gp alter constraint gp_a_check not enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'gp_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; +drop table gp cascade; + --for "no inherit" check constraint, it will not recurse to child table create table p1(f1 int constraint p1_a_check check (f1 > 0) no inherit not enforced); create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced); From d16be8605f5f699020fe4e6eaef008a41407a9bb Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Sat, 27 Jun 2026 17:43:02 -0400 Subject: [PATCH 05/36] doc: Clarify inherited constraint behavior Update the table inheritance documentation to mention not-null constraints alongside check constraints where inherited constraints are discussed. Also clarify that some properties of inherited constraints can now be altered directly on child tables, while the resulting constraint must remain compatible with its inherited parent constraints. For multiple inheritance, say explicitly that when a column or constraint is inherited from more than one parent, the stricter definition applies. Author: Chao Li Reviewed-by: Zsolt Parragi Discussion: https://postgr.es/m/E74C57FA-1DD0-4C8E-8FB1-538034752592@gmail.com --- doc/src/sgml/ddl.sgml | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 45db20d47d88d..fbd0ebbf10f40 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -4090,7 +4090,9 @@ VALUES ('Albany', NULL, NULL, 'NY'); similar fashion. Thus, for example, a merged column will be marked not-null if any one of the column definitions it came from is marked not-null. Check constraints are merged if they have the same name, - and the merge will fail if their conditions are different. + and the merge will fail if their conditions are different. For merged + check constraints, stricter enforceability is preserved: if any inherited + copy is enforced, the merged constraint is enforced. @@ -4104,8 +4106,9 @@ VALUES ('Albany', NULL, NULL, 'NY'); To do this the new child table must already include columns with the same names and types as the columns of the parent. It must also include check constraints with the same names and check expressions as those of the - parent. Similarly an inheritance link can be removed from a child using the - NO INHERIT variant of ALTER TABLE. + parent, as well as matching not-null constraints. Similarly an inheritance + link can be removed from a child using the NO INHERIT + variant of ALTER TABLE. Dynamically adding and removing inheritance links like this can be useful when the inheritance relationship is being used for table partitioning (see ). @@ -4124,21 +4127,23 @@ VALUES ('Albany', NULL, NULL, 'NY'); A parent table cannot be dropped while any of its children remain. Neither - can columns or check constraints of child tables be dropped or altered - if they are inherited - from any parent tables. If you wish to remove a table and all of its - descendants, one easy way is to drop the parent table with the - CASCADE option (see ). + can inherited columns or inherited check and not-null constraints of child + tables be dropped directly. Some properties of inherited constraints can + be altered, but each resulting constraint must remain compatible with all + parent constraints from which it is inherited. If you wish to remove a + table and all of its descendants, one easy way is to drop the parent table + with the CASCADE option (see ). ALTER TABLE will - propagate any changes in column data definitions and check - constraints down the inheritance hierarchy. Again, dropping - columns that are depended on by other tables is only possible when using - the CASCADE option. ALTER - TABLE follows the same rules for duplicate column merging - and rejection that apply during CREATE TABLE. + propagate changes in column definitions and in inheritable constraints + (check and not-null constraints) down the inheritance hierarchy. Again, + dropping columns that are depended on by other tables is only possible + when using the CASCADE option. ALTER + TABLE follows the same rules for merging or rejecting duplicate + inherited column and constraint definitions that apply during + CREATE TABLE. From f03ecd26396423cc38898b2479586c7c89ac9a1c Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Sat, 27 Jun 2026 17:43:02 -0400 Subject: [PATCH 06/36] doc: Clarify ALTER CONSTRAINT enforceability behavior The ALTER TABLE documentation said that FOREIGN KEY and CHECK constraints may be altered, but did not distinguish between deferrability and enforceability attributes. Clarify that deferrability attributes can currently be altered only for FOREIGN KEY constraints, while enforceability can be altered for both FOREIGN KEY and CHECK constraints. Also document that setting a constraint to ENFORCED verifies existing rows and resumes checking new or updated rows. Author: Chao Li Reviewed-by: Zsolt Parragi Discussion: https://postgr.es/m/E74C57FA-1DD0-4C8E-8FB1-538034752592@gmail.com Discussion: https://postgr.es/m/711B1ED3-1781-4B6C-A573-B58AF20770E5@gmail.com --- doc/src/sgml/ref/alter_table.sgml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 67a05593140c5..6dd518752c003 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -586,8 +586,18 @@ WITH ( MODULUS numeric_literal, REM This form alters the attributes of a constraint that was previously - created. Currently FOREIGN KEY and CHECK - constraints may be altered in this fashion, but see below. + created. Currently, the deferrability attributes can be altered only + for FOREIGN KEY constraints. The enforceability + attribute can be altered for FOREIGN KEY and + CHECK constraints. + + + + Setting a constraint to NOT ENFORCED causes the + database system to stop checking it for new or updated rows. Setting + a constraint to ENFORCED causes the database system + to verify that existing rows satisfy the constraint and to check it + for new or updated rows. From 02f699c1416380c0411fbc0f53061f86b2f52ed3 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Sat, 27 Jun 2026 19:57:41 -0400 Subject: [PATCH 07/36] pgindent fix for commit effb923d9de --- src/backend/commands/copyto.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index d30cad019c739..d3adc752ae30c 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -1052,11 +1052,11 @@ BeginCopyTo(ParseState *pstate, cstate->json_buf = makeStringInfo(); /* - * Build a projected TupleDesc describing only the selected columns - * so that composite_to_json() emits the right column names and - * types; needed when an explicit column list was given (possibly - * with a different column order) or when generated columns are - * excluded from the output. + * Build a projected TupleDesc describing only the selected columns so + * that composite_to_json() emits the right column names and types; + * needed when an explicit column list was given (possibly with a + * different column order) or when generated columns are excluded from + * the output. */ if (rel && (attnamelist != NIL || list_length(cstate->attnumlist) < tupDesc->natts)) From d6ed87d19890b1cfa93d1f6e8957fa525834c0e2 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Fri, 26 Jun 2026 08:00:39 -0400 Subject: [PATCH 08/36] Use named boolean parameters for pg_get_*_ddl option arguments Replace the VARIADIC text[] alternating key/value option interface with typed named boolean parameters for pg_get_role_ddl(), pg_get_tablespace_ddl(), and pg_get_database_ddl(), as added by commit 4881981f920 and friends. The new signatures are: pg_get_role_ddl(role regrole, pretty boolean DEFAULT false, memberships boolean DEFAULT true) pg_get_tablespace_ddl(tablespace oid/name, pretty boolean DEFAULT false, owner boolean DEFAULT true) pg_get_database_ddl(db regdatabase, pretty boolean DEFAULT false, owner boolean DEFAULT true, tablespace boolean DEFAULT true) This provides type safety at the SQL level, allows named-argument calling syntax (pretty => true), removes the runtime string-parsing machinery (DdlOption, parse_ddl_options) in favour of direct PG_GETARG_BOOL() calls, and allows the functions to be marked STRICT. While we're here, I added an extra TAP test for pg_get_database(owner => false, ...) Catalog version bumped. Author: Jelte Fennema-Nio Discussion: https://postgr.es/m/DHM6C7SLS4BN.1WW9Z4PRPN0VJ@jeltef.nl (and on Discord) --- doc/src/sgml/func/func-info.sgml | 54 ++-- src/backend/utils/adt/ddlutils.c | 260 ++----------------- src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_proc.dat | 41 ++- src/test/modules/test_misc/t/012_ddlutils.pl | 28 +- 5 files changed, 95 insertions(+), 290 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index 211bc8b238b92..bc80bcbd0b3cf 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -3856,9 +3856,7 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} reconstruct DDL statements for various global database objects. Each function returns a set of text rows, one SQL statement per row. (This is a decompiled reconstruction, not the original text of the - command.) Functions that accept VARIADIC options - take alternating name/value text pairs; values are parsed as boolean, - integer or text. + command.) @@ -3883,8 +3881,10 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} pg_get_role_ddl ( roleregrole - , VARIADIC options - text[] ) + , pretty boolean + DEFAULT false + , memberships boolean + DEFAULT true ) setof text @@ -3892,10 +3892,10 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} ALTER ROLE ... SET statements for the given role. Each statement is returned as a separate row. Password information is never included in the output. - The following options are supported: pretty (boolean) - for pretty-printed output and memberships (boolean, - default true) to include GRANT statements for - role memberships and their options. + When pretty is true, the output is + pretty-printed. When memberships is false, + GRANT statements for role memberships are + omitted. @@ -3905,15 +3905,19 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} pg_get_tablespace_ddl ( tablespace oid - , VARIADIC options - text[] ) + , pretty boolean + DEFAULT false + , owner boolean + DEFAULT true ) setof text pg_get_tablespace_ddl ( tablespace name - , VARIADIC options - text[] ) + , pretty boolean + DEFAULT false + , owner boolean + DEFAULT true ) setof text @@ -3921,9 +3925,9 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} the specified tablespace (by OID or name). If the tablespace has options set, an ALTER TABLESPACE ... SET statement is also returned. Each statement is returned as a separate row. - The following options are supported: pretty (boolean) - for formatted output and owner (boolean) to include - OWNER. + When pretty is true, the output is + pretty-printed. When owner is false, the + OWNER clause is omitted. @@ -3933,8 +3937,12 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} pg_get_database_ddl ( database regdatabase - , VARIADIC options - text[] ) + , pretty boolean + DEFAULT false + , owner boolean + DEFAULT true + , tablespace boolean + DEFAULT true ) setof text @@ -3942,11 +3950,11 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} specified database, followed by ALTER DATABASE statements for connection limit, template status, and configuration settings. Each statement is returned as a separate row. - The following options are supported: - pretty (boolean) for formatted output, - owner (boolean) to include OWNER, - and tablespace (boolean) to include - TABLESPACE. + When pretty is true, the output is + pretty-printed. When owner is false, the + OWNER clause is omitted. When + tablespace is false, the + TABLESPACE clause is omitted. diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c index f32fcd453efa3..a70f1c2865501 100644 --- a/src/backend/utils/adt/ddlutils.c +++ b/src/backend/utils/adt/ddlutils.c @@ -33,7 +33,6 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "utils/acl.h" -#include "utils/array.h" #include "utils/builtins.h" #include "utils/datetime.h" #include "utils/fmgroids.h" @@ -46,35 +45,6 @@ #include "utils/timestamp.h" #include "utils/varlena.h" -/* Option value types for DDL option parsing */ -typedef enum -{ - DDL_OPT_BOOL, - DDL_OPT_TEXT, - DDL_OPT_INT, -} DdlOptType; - -/* - * A single DDL option descriptor: caller fills in name and type, - * parse_ddl_options fills in isset + the appropriate value field. - */ -typedef struct DdlOption -{ - const char *name; /* option name (case-insensitive match) */ - DdlOptType type; /* expected value type */ - bool isset; /* true if caller supplied this option */ - /* fields for specific option types */ - union - { - bool boolval; /* filled in for DDL_OPT_BOOL */ - char *textval; /* filled in for DDL_OPT_TEXT (palloc'd) */ - int intval; /* filled in for DDL_OPT_INT */ - }; -} DdlOption; - - -static void parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start, - DdlOption *opts, int nopts); static void append_ddl_option(StringInfo buf, bool pretty, int indent, const char *fmt, ...) pg_attribute_printf(4, 5); @@ -83,150 +53,11 @@ static void append_guc_value(StringInfo buf, const char *name, static List *pg_get_role_ddl_internal(Oid roleid, bool pretty, bool memberships); static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner); -static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull); +static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid); static List *pg_get_database_ddl_internal(Oid dbid, bool pretty, bool no_owner, bool no_tablespace); -/* - * parse_ddl_options - * Parse variadic name/value option pairs - * - * Options are passed as alternating key/value text pairs. The caller - * provides an array of DdlOption descriptors specifying the accepted - * option names and their types; this function matches each supplied - * pair against the array, validates the value, and fills in the - * result fields. - */ -static void -parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start, - DdlOption *opts, int nopts) -{ - Datum *args; - bool *nulls; - Oid *types; - int nargs; - - /* Clear all output fields */ - for (int i = 0; i < nopts; i++) - { - opts[i].isset = false; - switch (opts[i].type) - { - case DDL_OPT_BOOL: - opts[i].boolval = false; - break; - case DDL_OPT_TEXT: - opts[i].textval = NULL; - break; - case DDL_OPT_INT: - opts[i].intval = 0; - break; - } - } - - nargs = extract_variadic_args(fcinfo, variadic_start, true, - &args, &types, &nulls); - - if (nargs <= 0) - return; - - /* Handle DEFAULT NULL case */ - if (nargs == 1 && nulls[0]) - return; - - if (nargs % 2 != 0) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("variadic arguments must be name/value pairs"), - errhint("Provide an even number of variadic arguments that can be divided into pairs."))); - - /* - * For each option name/value pair, find corresponding positional option - * for the option name, and assign the option value. - */ - for (int i = 0; i < nargs; i += 2) - { - char *name; - char *valstr; - DdlOption *opt = NULL; - - if (nulls[i]) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("option name at variadic position %d is null", i + 1))); - - name = TextDatumGetCString(args[i]); - - if (nulls[i + 1]) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("value for option \"%s\" must not be null", name))); - - /* Find matching option descriptor */ - for (int j = 0; j < nopts; j++) - { - if (pg_strcasecmp(name, opts[j].name) == 0) - { - opt = &opts[j]; - break; - } - } - - if (opt == NULL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized option: \"%s\"", name))); - - if (opt->isset) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("option \"%s\" is specified more than once", - name))); - - valstr = TextDatumGetCString(args[i + 1]); - - switch (opt->type) - { - case DDL_OPT_BOOL: - if (!parse_bool(valstr, &opt->boolval)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid value for boolean option \"%s\": %s", - name, valstr))); - break; - - case DDL_OPT_TEXT: - opt->textval = valstr; - valstr = NULL; /* don't pfree below */ - break; - - case DDL_OPT_INT: - { - char *endp; - long val; - - errno = 0; - val = strtol(valstr, &endp, 10); - if (*endp != '\0' || errno == ERANGE || - val < PG_INT32_MIN || val > PG_INT32_MAX) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid value for integer option \"%s\": %s", - name, valstr))); - opt->intval = (int) val; - } - break; - } - - opt->isset = true; - - if (valstr) - pfree(valstr); - pfree(name); - } -} - /* * Helper to append a formatted string with optional pretty-printing. */ @@ -590,7 +421,6 @@ pg_get_role_ddl_internal(Oid roleid, bool pretty, bool memberships) * Each row is a complete SQL statement. The first row is always the * CREATE ROLE statement; subsequent rows are ALTER ROLE SET statements * and optionally GRANT statements for role memberships. - * Returns no rows if the role argument is NULL. */ Datum pg_get_role_ddl(PG_FUNCTION_ARGS) @@ -602,26 +432,17 @@ pg_get_role_ddl(PG_FUNCTION_ARGS) { MemoryContext oldcontext; Oid roleid; - DdlOption opts[] = { - {"pretty", DDL_OPT_BOOL}, - {"memberships", DDL_OPT_BOOL}, - }; + bool pretty; + bool memberships; funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - if (PG_ARGISNULL(0)) - { - MemoryContextSwitchTo(oldcontext); - SRF_RETURN_DONE(funcctx); - } - roleid = PG_GETARG_OID(0); - parse_ddl_options(fcinfo, 1, opts, lengthof(opts)); + pretty = PG_GETARG_BOOL(1); + memberships = PG_GETARG_BOOL(2); - statements = pg_get_role_ddl_internal(roleid, - opts[0].isset && opts[0].boolval, - !opts[1].isset || opts[1].boolval); + statements = pg_get_role_ddl_internal(roleid, pretty, memberships); funcctx->user_fctx = statements; funcctx->max_calls = list_length(statements); @@ -755,7 +576,7 @@ pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner) * pg_get_tablespace_ddl_srf - common SRF logic for tablespace DDL */ static Datum -pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull) +pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid) { FuncCallContext *funcctx; List *statements; @@ -763,25 +584,16 @@ pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull) if (SRF_IS_FIRSTCALL()) { MemoryContext oldcontext; - DdlOption opts[] = { - {"pretty", DDL_OPT_BOOL}, - {"owner", DDL_OPT_BOOL}, - }; + bool pretty; + bool no_owner; funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - if (isnull) - { - MemoryContextSwitchTo(oldcontext); - SRF_RETURN_DONE(funcctx); - } - - parse_ddl_options(fcinfo, 1, opts, lengthof(opts)); + pretty = PG_GETARG_BOOL(1); + no_owner = !PG_GETARG_BOOL(2); - statements = pg_get_tablespace_ddl_internal(tsid, - opts[0].isset && opts[0].boolval, - opts[1].isset && !opts[1].boolval); + statements = pg_get_tablespace_ddl_internal(tsid, pretty, no_owner); funcctx->user_fctx = statements; funcctx->max_calls = list_length(statements); @@ -813,14 +625,9 @@ pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull) Datum pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS) { - Oid tsid = InvalidOid; - bool isnull; - - isnull = PG_ARGISNULL(0); - if (!isnull) - tsid = PG_GETARG_OID(0); + Oid tsid = PG_GETARG_OID(0); - return pg_get_tablespace_ddl_srf(fcinfo, tsid, isnull); + return pg_get_tablespace_ddl_srf(fcinfo, tsid); } /* @@ -830,19 +637,10 @@ pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS) Datum pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS) { - Oid tsid = InvalidOid; - Name tspname; - bool isnull; + Name tspname = PG_GETARG_NAME(0); + Oid tsid = get_tablespace_oid(NameStr(*tspname), false); - isnull = PG_ARGISNULL(0); - - if (!isnull) - { - tspname = PG_GETARG_NAME(0); - tsid = get_tablespace_oid(NameStr(*tspname), false); - } - - return pg_get_tablespace_ddl_srf(fcinfo, tsid, isnull); + return pg_get_tablespace_ddl_srf(fcinfo, tsid); } /* @@ -1140,28 +938,20 @@ pg_get_database_ddl(PG_FUNCTION_ARGS) { MemoryContext oldcontext; Oid dbid; - DdlOption opts[] = { - {"pretty", DDL_OPT_BOOL}, - {"owner", DDL_OPT_BOOL}, - {"tablespace", DDL_OPT_BOOL}, - }; + bool pretty; + bool no_owner; + bool no_tablespace; funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - if (PG_ARGISNULL(0)) - { - MemoryContextSwitchTo(oldcontext); - SRF_RETURN_DONE(funcctx); - } - dbid = PG_GETARG_OID(0); - parse_ddl_options(fcinfo, 1, opts, lengthof(opts)); + pretty = PG_GETARG_BOOL(1); + no_owner = !PG_GETARG_BOOL(2); + no_tablespace = !PG_GETARG_BOOL(3); - statements = pg_get_database_ddl_internal(dbid, - opts[0].isset && opts[0].boolval, - opts[1].isset && !opts[1].boolval, - opts[2].isset && !opts[2].boolval); + statements = pg_get_database_ddl_internal(dbid, pretty, no_owner, + no_tablespace); funcctx->user_fctx = statements; funcctx->max_calls = list_length(statements); diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 2fe3be9ada58c..635c0d9cb13e7 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202606251 +#define CATALOG_VERSION_NO 202606281 #endif diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 384ba908d35bc..402d869710bd7 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8591,30 +8591,29 @@ proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' }, { oid => '6501', descr => 'get DDL to recreate a role', - proname => 'pg_get_role_ddl', prorows => '10', provariadic => 'text', - proisstrict => 'f', proretset => 't', provolatile => 's', - pronargdefaults => '1', prorettype => 'text', proargtypes => 'regrole _text', - proallargtypes => '{regrole,_text}', proargmodes => '{i,v}', - proargdefaults => '{NULL}', prosrc => 'pg_get_role_ddl' }, + proname => 'pg_get_role_ddl', prorows => '10', proisstrict => 't', + proretset => 't', provolatile => 's', pronargdefaults => '2', + prorettype => 'text', proargtypes => 'regrole bool bool', + proargnames => '{role,pretty,memberships}', + proargdefaults => '{false,true}', prosrc => 'pg_get_role_ddl' }, { oid => '6499', descr => 'get DDL to recreate a tablespace', - proname => 'pg_get_tablespace_ddl', prorows => '10', provariadic => 'text', - proisstrict => 'f', proretset => 't', provolatile => 's', - pronargdefaults => '1', prorettype => 'text', proargtypes => 'oid _text', - proallargtypes => '{oid,_text}', proargmodes => '{i,v}', - proargdefaults => '{NULL}', prosrc => 'pg_get_tablespace_ddl_oid' }, + proname => 'pg_get_tablespace_ddl', prorows => '10', proisstrict => 't', + proretset => 't', provolatile => 's', pronargdefaults => '2', + prorettype => 'text', proargtypes => 'oid bool bool', + proargnames => '{tablespace,pretty,owner}', + proargdefaults => '{false,true}', prosrc => 'pg_get_tablespace_ddl_oid' }, { oid => '6500', descr => 'get DDL to recreate a tablespace', - proname => 'pg_get_tablespace_ddl', prorows => '10', provariadic => 'text', - proisstrict => 'f', proretset => 't', provolatile => 's', - pronargdefaults => '1', prorettype => 'text', proargtypes => 'name _text', - proallargtypes => '{name,_text}', proargmodes => '{i,v}', - proargdefaults => '{NULL}', prosrc => 'pg_get_tablespace_ddl_name' }, + proname => 'pg_get_tablespace_ddl', prorows => '10', proisstrict => 't', + proretset => 't', provolatile => 's', pronargdefaults => '2', + prorettype => 'text', proargtypes => 'name bool bool', + proargnames => '{tablespace,pretty,owner}', + proargdefaults => '{false,true}', prosrc => 'pg_get_tablespace_ddl_name' }, { oid => '6502', descr => 'get DDL to recreate a database', - proname => 'pg_get_database_ddl', prorows => '10', provariadic => 'text', - proisstrict => 'f', proretset => 't', provolatile => 's', - pronargdefaults => '1', prorettype => 'text', - proargtypes => 'regdatabase _text', proallargtypes => '{regdatabase,_text}', - proargmodes => '{i,v}', proargdefaults => '{NULL}', - prosrc => 'pg_get_database_ddl' }, + proname => 'pg_get_database_ddl', prorows => '10', proisstrict => 't', + proretset => 't', provolatile => 's', pronargdefaults => '3', + prorettype => 'text', proargtypes => 'regdatabase bool bool bool', + proargnames => '{database,pretty,owner,tablespace}', + proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' }, { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', diff --git a/src/test/modules/test_misc/t/012_ddlutils.pl b/src/test/modules/test_misc/t/012_ddlutils.pl index bcdb831a6761a..e541dfa38d12f 100644 --- a/src/test/modules/test_misc/t/012_ddlutils.pl +++ b/src/test/modules/test_misc/t/012_ddlutils.pl @@ -93,7 +93,7 @@ sub ddl_filter # Pretty-printed output $result = $node->safe_psql('postgres', - q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_test2', 'pretty', 'true')} + q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_test2', pretty => true)} ); like($result, qr/\n\s+SUPERUSER/, 'role pretty-print indents attributes'); @@ -123,7 +123,7 @@ sub ddl_filter # Memberships suppressed $result = $node->safe_psql('postgres', - q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_member', 'memberships', 'false')} + q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_member', memberships => false)} ); unlike($result, qr/GRANT/, 'memberships suppressed'); @@ -176,18 +176,18 @@ sub ddl_filter q{SELECT count(*) FROM pg_get_database_ddl(NULL)}); is($result, '0', 'NULL database returns no rows'); -# Invalid option +# Invalid option (bad boolean cast) ($ret, $stdout, $stderr) = $node->psql('postgres', - q{SELECT * FROM pg_get_database_ddl('regression_ddlutils_test', 'owner', 'invalid')} + q{SELECT * FROM pg_get_database_ddl('regression_ddlutils_test', owner => 'invalid')} ); isnt($ret, 0, 'invalid boolean option errors'); -like($stderr, qr/invalid value/, 'invalid option error message'); +like($stderr, qr/invalid input syntax for type boolean/, 'invalid option error message'); -# Duplicate option +# Duplicate named argument ($ret, $stdout, $stderr) = $node->psql( 'postgres', q{SELECT * FROM pg_get_database_ddl('regression_ddlutils_test', - 'owner', 'false', 'owner', 'true')}); + owner => false, owner => true)}); isnt($ret, 0, 'duplicate option errors'); # Basic output (without locale details) @@ -218,9 +218,17 @@ sub ddl_filter 'postgres', q{SELECT pg_get_database_ddl FROM pg_get_database_ddl('regression_ddlutils_test', - 'pretty', 'true', 'tablespace', 'false')})); + pretty => true, tablespace => false)})); like($result, qr/\n\s+WITH TEMPLATE/, 'database DDL pretty-prints WITH'); +# Owner suppressed +$result = ddl_filter( + $node->safe_psql( + 'postgres', + q{SELECT pg_get_database_ddl + FROM pg_get_database_ddl('regression_ddlutils_test', owner => false)})); +unlike($result, qr/OWNER/, 'database DDL owner suppressed'); + # Permission check $node->safe_psql( 'postgres', q{ @@ -292,14 +300,14 @@ sub ddl_filter $result = $node->safe_psql( 'postgres', q{SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', - 'pretty', 'true')}); + pretty => true)}); like($result, qr/\n\s+OWNER/, 'tablespace DDL pretty-prints OWNER'); # Owner suppressed $result = $node->safe_psql( 'postgres', q{SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', - 'owner', 'false')}); + owner => false)}); unlike($result, qr/OWNER/, 'tablespace DDL owner suppressed'); # Lookup by OID From b574fec00f275e50ffe2c9780ec1f6398796c905 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 28 Jun 2026 12:31:29 -0400 Subject: [PATCH 09/36] Avoid collation lookup failure when considering a "char" column. If a "char" column has a statistics histogram, scalarineqsel() would fail with "cache lookup failed for collation 0". Avoid the failing lookup by acting as though the collation is "C". Prior to commit 06421b084, this code didn't fail because lc_collate_is_c() intentionally didn't spit up on InvalidOid. It did act differently though: it would take the non-C-collation code path and hence apply strxfrm using libc's prevailing locale. But that seems like the wrong thing for a non-collatable comparison, so let's not resurrect that aspect. Author: Feng Wu Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CACK3muq6s-O1Wc3w4dRL1Fe8YQ-Fz1zJbezeQwhuLgNxGNEFiA@mail.gmail.com Backpatch-through: 18 --- src/backend/utils/adt/selfuncs.c | 8 ++++++++ src/test/regress/expected/planner_est.out | 11 +++++++++++ src/test/regress/sql/planner_est.sql | 6 ++++++ 3 files changed, 25 insertions(+) diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index d6efd07073a1e..cbc70fde7168e 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -5295,6 +5295,14 @@ convert_string_datum(Datum value, Oid typid, Oid collid, bool *failure) return NULL; } + /* + * If we don't have a collation, act as though it's "C". This would + * normally happen only for the "char" type, but perhaps there are other + * cases. + */ + if (!OidIsValid(collid)) + return val; + mylocale = pg_newlocale_from_collation(collid); if (!mylocale->collate_is_c) diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out index b62a47552fad4..236cb274a78d9 100644 --- a/src/test/regress/expected/planner_est.out +++ b/src/test/regress/expected/planner_est.out @@ -210,4 +210,15 @@ false, true, false, true); -> Result (cost=N..N rows=1 width=N) (4 rows) +-- Verify that scalarineqsel() works on "char" columns +CREATE TEMP TABLE char_table_1 AS + SELECT i::"char" AS c FROM generate_series(64,96) i; +ANALYZE char_table_1; +EXPLAIN (COSTS OFF) SELECT * FROM char_table_1 WHERE c < 'Q'; + QUERY PLAN +----------------------------- + Seq Scan on char_table_1 + Filter: (c < 'Q'::"char") +(2 rows) + DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); diff --git a/src/test/regress/sql/planner_est.sql b/src/test/regress/sql/planner_est.sql index 53210d5baad8e..2b696a4e4e55d 100644 --- a/src/test/regress/sql/planner_est.sql +++ b/src/test/regress/sql/planner_est.sql @@ -147,4 +147,10 @@ SELECT explain_mask_costs($$ SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 98, (SELECT 99), NULL]);$$, false, true, false, true); +-- Verify that scalarineqsel() works on "char" columns +CREATE TEMP TABLE char_table_1 AS + SELECT i::"char" AS c FROM generate_series(64,96) i; +ANALYZE char_table_1; +EXPLAIN (COSTS OFF) SELECT * FROM char_table_1 WHERE c < 'Q'; + DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); From e42d4a1f3dc59420be796883404020cb41ddd05e Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Sun, 28 Jun 2026 23:43:19 +0200 Subject: [PATCH 10/36] doc: Improve consistency in varlistentry attributes Use underscores consistent across the varlistentry attributes in config.sgml. Inconsistencies found using Claude, verified by the reporter. Reported-by: Bill Kim Reviewed-by: Peter Smith Discussion: https://postgr.es/m/CAMQXxchB0ZfMHyk+Ji-=s3hkqh0_XyuKiaNLRgvatvndSt3KNw@mail.gmail.com --- doc/src/sgml/config.sgml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index fa566c9e5535a..569fc0e7dbaa6 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -5842,7 +5842,7 @@ ANY num_sync ( + enable_group_by_reordering (boolean) enable_group_by_reordering configuration parameter @@ -11894,7 +11894,7 @@ dynamic_library_path = '/usr/local/lib/postgresql:$libdir' - + quote_all_identifiers (boolean) quote_all_identifiers configuration parameter From 6f4bac854fb784f83b86f05c7e9921e038135442 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Mon, 29 Jun 2026 10:24:28 +0900 Subject: [PATCH 11/36] Hardwire RI fast-path end-of-xact cleanup into xact.c Commit b7b27eb41a5, which added foreign-key fast-path batching to ri_triggers.c, registered ri_FastPathXactCallback() via RegisterXactCallback() to clear the fast-path batching state at end of transaction. RegisterXactCallback() is documented as intended for dynamically loaded modules; built-in code is supposed to hardwire its end-of-xact hooks into xact.c, mainly so callback ordering can be controlled where it matters (see the header comment on RegisterXactCallback()). Convert the callback into a plain AtEOXact_RI() function and call it directly from CommitTransaction(), PrepareTransaction() and AbortTransaction(), alongside the other AtEOXact_* cleanup steps, and drop the RegisterXactCallback() registration. Like the other AtEOXact_* routines, AtEOXact_RI() takes an isCommit argument and treats the two paths differently. On commit or prepare the fast-path cache must already have been flushed and torn down by the after-trigger batch callback, so a surviving cache indicates a trigger batch was never flushed -- which would have silently skipped FK checks -- and draws an Assert plus a WARNING. On abort a surviving cache is expected (a flush may have errored out partway) and is simply reset. There is no ordering dependency here: AtEOXact_RI() only resets backend-local static state (the cache pointer, the callback-registered flag, and the in-flush guard). It touches no relations, locks, buffers or catalogs, so its position relative to ResourceOwnerRelease() and the surrounding AtEOXact_* calls does not matter. On a normal commit the fast-path cache has already been flushed and torn down by ri_FastPathEndBatch() (an AfterTriggerBatchCallback fired from AfterTriggerFireDeferred(), well before any end-of-xact callback), so the reset is a no-op; its real job is the abort path, where teardown may not have run and the static pointers would otherwise dangle into the next transaction. The cache memory itself lives in TopTransactionContext and is freed by the end-of-transaction memory-context reset on both paths. The companion RegisterSubXactCallback() use from b7b27eb41a5 was already removed by commit 4113873a, which confined fast-path batching to the top transaction level, so only the RegisterXactCallback() use remained. Reported-by: Bertrand Drouvot Reviewed-by: Bertrand Drouvot Discussion: https://postgr.es/m/ajypPeEWceXRGAEW@bdtpg --- src/backend/access/transam/xact.c | 3 ++ src/backend/utils/adt/ri_triggers.c | 58 +++++++++++++++++++++-------- src/include/commands/trigger.h | 2 + 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index de4cf96eaa2ca..3a89149016fe6 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -2514,6 +2514,7 @@ CommitTransaction(void) AtEOXact_Files(true); AtEOXact_ComboCid(); AtEOXact_HashTables(true); + AtEOXact_RI(true); AtEOXact_PgStat(true, is_parallel_worker); AtEOXact_Snapshot(true, false); AtEOXact_ApplyLauncher(true); @@ -2809,6 +2810,7 @@ PrepareTransaction(void) AtEOXact_Files(true); AtEOXact_ComboCid(); AtEOXact_HashTables(true); + AtEOXact_RI(true); /* don't call AtEOXact_PgStat here; we fixed pgstat state above */ AtEOXact_Snapshot(true, true); /* we treat PREPARE as ROLLBACK so far as waking workers goes */ @@ -3039,6 +3041,7 @@ AbortTransaction(void) AtEOXact_Files(false); AtEOXact_ComboCid(); AtEOXact_HashTables(false); + AtEOXact_RI(false); AtEOXact_PgStat(false, is_parallel_worker); AtEOXact_ApplyLauncher(false); AtEOXact_LogicalRepWorkers(false); diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 44129a35c0895..bf54f9b459243 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -228,7 +228,7 @@ typedef struct RI_CompareHashEntry * relations are held open with locks for the transaction duration, preventing * relcache invalidation. The entry itself is torn down at batch end by * ri_FastPathEndBatch(); on abort, ResourceOwner releases the cached - * relations and the XactCallback NULLs the static cache pointer to prevent + * relations and AtEOXact_RI() NULLs the static cache pointer to prevent * any subsequent access. */ typedef struct RI_FastPathEntry @@ -4187,8 +4187,8 @@ RI_FKey_trigger_type(Oid tgfoid) * Registered as an AfterTriggerBatchCallback. Note: the flush can * do real work (CCI, security context switch, index probes) and can * throw ERROR on a constraint violation. If that happens, - * ri_FastPathTeardown never runs; ResourceOwner + XactCallback - * handle resource cleanup on the abort path. + * ri_FastPathTeardown never runs; ResourceOwner releases the cached + * relations and AtEOXact_RI() resets the static state on the abort path. */ static void ri_FastPathEndBatch(void *arg) @@ -4273,15 +4273,47 @@ ri_FastPathTeardown(void) ri_fastpath_callback_registered = false; } -static bool ri_fastpath_xact_callback_registered = false; - -static void -ri_FastPathXactCallback(XactEvent event, void *arg) +/* + * AtEOXact_RI + * Reset fast-path batching state at end of transaction. + * + * Called from CommitTransaction() and PrepareTransaction() with isCommit + * true, and from AbortTransaction() with isCommit false. + * + * By the time we get here on a clean commit or prepare, the fast-path cache + * has already been flushed and torn down by ri_FastPathEndBatch() (an + * AfterTriggerBatchCallback fired from AfterTriggerFireDeferred(), well before + * this point), so the static pointers are already clear and the reset below is + * a no-op. A surviving cache at commit means a trigger batch was never + * flushed, which would have silently skipped FK checks, so we complain. + * + * On abort, ri_FastPathEndBatch()/ri_FastPathTeardown() may not have run (a + * flush can error out partway): the ResourceOwner releases the cached + * relations and the TopTransactionContext reset frees the cache memory, but + * the process-local static pointers below would dangle into the next + * transaction. This resets them so they don't. + * + * The reset touches only backend-local static state (no relations, locks, + * buffers or catalog access), so it has no ordering dependency on the + * surrounding ResourceOwnerRelease() / AtEOXact_* steps. + */ +void +AtEOXact_RI(bool isCommit) { /* - * On abort, ResourceOwner already released relations; on commit, - * ri_FastPathTeardown already ran. Either way, just NULL the static - * pointers so they don't dangle into the next transaction. + * The cache must be empty on a clean commit or prepare; a survivor means + * a trigger batch went unflushed. Assert for assert-enabled builds and, + * since the transaction is already committed by now and FK checks may + * have been skipped, also warn in production builds. + */ + Assert(ri_fastpath_cache == NULL || !isCommit); + if (isCommit && ri_fastpath_cache != NULL) + elog(WARNING, "RI fast-path cache not flushed at end of transaction"); + + /* + * Clear the static pointers/flags. The cache memory lives in + * TopTransactionContext and is freed by the end-of-transaction + * memory-context reset; here we only drop the references to it. */ ri_fastpath_cache = NULL; ri_fastpath_callback_registered = false; @@ -4316,12 +4348,6 @@ ri_FastPathGetEntry(const RI_ConstraintInfo *riinfo, Relation fk_rel) { HASHCTL ctl; - if (!ri_fastpath_xact_callback_registered) - { - RegisterXactCallback(ri_FastPathXactCallback, NULL); - ri_fastpath_xact_callback_registered = true; - } - ctl.keysize = sizeof(Oid); ctl.entrysize = sizeof(RI_FastPathEntry); ctl.hcxt = TopTransactionContext; diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index 1d9869973c094..0c3d485abf47f 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -310,4 +310,6 @@ extern void RegisterAfterTriggerBatchCallback(AfterTriggerBatchCallback callback void *arg); extern bool AfterTriggerIsActive(void); +extern void AtEOXact_RI(bool isCommit); + #endif /* TRIGGER_H */ From 8612f0b7ce09212b0b80af925b0966bdbd46a60f Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Mon, 29 Jun 2026 11:38:39 +0900 Subject: [PATCH 12/36] plpython: Fix NULL pointer dereferences for broken sequence and mapping objects PL/Python and its hstore and jsonb transforms build SQL values from Python containers by calling Python C API functions that can return NULL, and in several places the result was used without first checking it. On the sequence side, PySequence_GetItem() is used when converting a returned sequence into a SQL array or composite value, when reading the argument list passed to plpy.execute() or plpy.cursor(), and when reading the list of type names given to plpy.prepare(). On the mapping side, the hstore and jsonb transforms call PyMapping_Size() and PyMapping_Items() and then index the result with PyList_GetItem() and PyTuple_GetItem(). All of these return NULL (or -1), with a Python exception set, for a broken object: for example one whose __getitem__() or items() raises, or which reports a length that disagrees with what it actually yields. The unchecked result was then dereferenced, crashing the backend. Fix this by checking the result of each call and reporting a regular error if it failed, so that the underlying Python exception is surfaced instead of taking down the session. Author: Richard Guo Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/CAMbWs49BKM9wP6m8bCXEpHwQKp7usvOGV6Jf=J7FYr_BCpxLqg@mail.gmail.com Backpatch-through: 14 --- .../expected/hstore_plpython.out | 65 ++++++++++++++ contrib/hstore_plpython/hstore_plpython.c | 16 ++++ .../hstore_plpython/sql/hstore_plpython.sql | 65 ++++++++++++++ .../expected/jsonb_plpython.out | 89 +++++++++++++++++++ contrib/jsonb_plpython/jsonb_plpython.c | 21 ++++- contrib/jsonb_plpython/sql/jsonb_plpython.sql | 77 ++++++++++++++++ .../plpython/expected/plpython_composite.out | 16 ++++ src/pl/plpython/expected/plpython_spi.out | 51 +++++++++++ src/pl/plpython/expected/plpython_types.out | 16 ++++ src/pl/plpython/plpy_cursorobject.c | 5 ++ src/pl/plpython/plpy_spi.c | 10 +++ src/pl/plpython/plpy_typeio.c | 9 +- src/pl/plpython/sql/plpython_composite.sql | 12 +++ src/pl/plpython/sql/plpython_spi.sql | 39 ++++++++ src/pl/plpython/sql/plpython_types.sql | 13 +++ 15 files changed, 500 insertions(+), 4 deletions(-) diff --git a/contrib/hstore_plpython/expected/hstore_plpython.out b/contrib/hstore_plpython/expected/hstore_plpython.out index 5fb56a2f65d10..5f8315e84dd40 100644 --- a/contrib/hstore_plpython/expected/hstore_plpython.out +++ b/contrib/hstore_plpython/expected/hstore_plpython.out @@ -43,6 +43,71 @@ SELECT test1bad(); ERROR: not a Python mapping CONTEXT: while creating return value PL/Python function "test1bad" +-- A mapping whose items() raises should be reported as an error, not crash +-- the backend +CREATE FUNCTION test1broken() RETURNS hstore +LANGUAGE plpython3u +TRANSFORM FOR TYPE hstore +AS $$ +class C(dict): + def items(self): + raise ValueError('items failed') +d = C() +d['x'] = 1 +return d +$$; +SELECT test1broken(); +ERROR: could not get items from Python mapping +CONTEXT: while creating return value +PL/Python function "test1broken" +-- Likewise for a mapping whose items() does not return key/value pairs +CREATE FUNCTION test1malformed() RETURNS hstore +LANGUAGE plpython3u +TRANSFORM FOR TYPE hstore +AS $$ +class C(dict): + def items(self): + return [42] +d = C() +d['x'] = 1 +return d +$$; +SELECT test1malformed(); +ERROR: items() of a Python mapping must return key/value pairs +CONTEXT: while creating return value +PL/Python function "test1malformed" +-- Likewise for a mapping whose items() yields fewer pairs than its length +CREATE FUNCTION test1short() RETURNS hstore +LANGUAGE plpython3u +TRANSFORM FOR TYPE hstore +AS $$ +class C(dict): + def items(self): + return [] +d = C() +d['x'] = 1 +return d +$$; +SELECT test1short(); +ERROR: items() of a Python mapping must return key/value pairs +CONTEXT: while creating return value +PL/Python function "test1short" +-- Likewise for a mapping whose __len__() raises +CREATE FUNCTION test1brokenlen() RETURNS hstore +LANGUAGE plpython3u +TRANSFORM FOR TYPE hstore +AS $$ +class C(dict): + def __len__(self): + raise ValueError('len failed') +d = C() +d['x'] = 1 +return d +$$; +SELECT test1brokenlen(); +ERROR: could not get size of Python mapping +CONTEXT: while creating return value +PL/Python function "test1brokenlen" -- test hstore[] -> python CREATE FUNCTION test1arr(val hstore[]) RETURNS int LANGUAGE plpython3u diff --git a/contrib/hstore_plpython/hstore_plpython.c b/contrib/hstore_plpython/hstore_plpython.c index f1e483980f4c7..b9d8b4537f781 100644 --- a/contrib/hstore_plpython/hstore_plpython.c +++ b/contrib/hstore_plpython/hstore_plpython.c @@ -143,7 +143,16 @@ plpython_to_hstore(PG_FUNCTION_ARGS) errmsg("not a Python mapping"))); pcount = PyMapping_Size(dict); + if (pcount < 0) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("could not get size of Python mapping"))); + items = PyMapping_Items(dict); + if (items == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("could not get items from Python mapping"))); PG_TRY(); { @@ -160,6 +169,13 @@ plpython_to_hstore(PG_FUNCTION_ARGS) PyObject *value; tuple = PyList_GetItem(items, i); + + /* The mapping's items() must yield key/value pairs */ + if (tuple == NULL || !PyTuple_Check(tuple) || PyTuple_Size(tuple) < 2) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("items() of a Python mapping must return key/value pairs"))); + key = PyTuple_GetItem(tuple, 0); value = PyTuple_GetItem(tuple, 1); diff --git a/contrib/hstore_plpython/sql/hstore_plpython.sql b/contrib/hstore_plpython/sql/hstore_plpython.sql index ebd61e6c467ac..a2b2046380f1a 100644 --- a/contrib/hstore_plpython/sql/hstore_plpython.sql +++ b/contrib/hstore_plpython/sql/hstore_plpython.sql @@ -38,6 +38,71 @@ $$; SELECT test1bad(); +-- A mapping whose items() raises should be reported as an error, not crash +-- the backend +CREATE FUNCTION test1broken() RETURNS hstore +LANGUAGE plpython3u +TRANSFORM FOR TYPE hstore +AS $$ +class C(dict): + def items(self): + raise ValueError('items failed') +d = C() +d['x'] = 1 +return d +$$; + +SELECT test1broken(); + + +-- Likewise for a mapping whose items() does not return key/value pairs +CREATE FUNCTION test1malformed() RETURNS hstore +LANGUAGE plpython3u +TRANSFORM FOR TYPE hstore +AS $$ +class C(dict): + def items(self): + return [42] +d = C() +d['x'] = 1 +return d +$$; + +SELECT test1malformed(); + + +-- Likewise for a mapping whose items() yields fewer pairs than its length +CREATE FUNCTION test1short() RETURNS hstore +LANGUAGE plpython3u +TRANSFORM FOR TYPE hstore +AS $$ +class C(dict): + def items(self): + return [] +d = C() +d['x'] = 1 +return d +$$; + +SELECT test1short(); + + +-- Likewise for a mapping whose __len__() raises +CREATE FUNCTION test1brokenlen() RETURNS hstore +LANGUAGE plpython3u +TRANSFORM FOR TYPE hstore +AS $$ +class C(dict): + def __len__(self): + raise ValueError('len failed') +d = C() +d['x'] = 1 +return d +$$; + +SELECT test1brokenlen(); + + -- test hstore[] -> python CREATE FUNCTION test1arr(val hstore[]) RETURNS int LANGUAGE plpython3u diff --git a/contrib/jsonb_plpython/expected/jsonb_plpython.out b/contrib/jsonb_plpython/expected/jsonb_plpython.out index cac963de69c9f..8d3f532880913 100644 --- a/contrib/jsonb_plpython/expected/jsonb_plpython.out +++ b/contrib/jsonb_plpython/expected/jsonb_plpython.out @@ -304,3 +304,92 @@ SELECT test_dict1(); {"": 2, "a": 1, "33": 3} (1 row) +-- A custom sequence whose __getitem__ raises should be reported as an error, +-- not crash the backend +CREATE FUNCTION test_broken_sequence() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C: + def __len__(self): + return 2 + def __getitem__(self, i): + raise ValueError('getitem failed') +return C() +$$; +SELECT test_broken_sequence(); +ERROR: could not get element 0 from sequence +DETAIL: ValueError: getitem failed +CONTEXT: Traceback (most recent call last): +while creating return value +PL/Python function "test_broken_sequence" +-- A mapping whose items() raises should be reported as an error, not crash +-- the backend +CREATE FUNCTION test_broken_mapping() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C(dict): + def items(self): + raise ValueError('items failed') +d = C() +d['x'] = 1 +return d +$$; +SELECT test_broken_mapping(); +ERROR: could not get items from Python mapping +DETAIL: ValueError: items failed +CONTEXT: Traceback (most recent call last): +while creating return value +PL/Python function "test_broken_mapping" +-- Likewise for a mapping whose items() does not return key/value pairs +CREATE FUNCTION test_malformed_mapping() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C(dict): + def items(self): + return [42] +d = C() +d['x'] = 1 +return d +$$; +SELECT test_malformed_mapping(); +ERROR: items() of a Python mapping must return key/value pairs +CONTEXT: while creating return value +PL/Python function "test_malformed_mapping" +-- Likewise for a mapping whose items() yields fewer pairs than its length +CREATE FUNCTION test_short_mapping() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C(dict): + def items(self): + return [] +d = C() +d['x'] = 1 +return d +$$; +SELECT test_short_mapping(); +ERROR: items() of a Python mapping must return key/value pairs +DETAIL: IndexError: list index out of range +CONTEXT: while creating return value +PL/Python function "test_short_mapping" +-- Likewise for a mapping whose __len__() raises +CREATE FUNCTION test_broken_len_mapping() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C(dict): + def __len__(self): + raise ValueError('len failed') +d = C() +d['x'] = 1 +return d +$$; +SELECT test_broken_len_mapping(); +ERROR: could not get size of Python mapping +DETAIL: ValueError: len failed +CONTEXT: Traceback (most recent call last): +while creating return value +PL/Python function "test_broken_len_mapping" diff --git a/contrib/jsonb_plpython/jsonb_plpython.c b/contrib/jsonb_plpython/jsonb_plpython.c index 909612a6039b1..dc17c9ee570da 100644 --- a/contrib/jsonb_plpython/jsonb_plpython.c +++ b/contrib/jsonb_plpython/jsonb_plpython.c @@ -274,7 +274,12 @@ PLyMapping_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state) PyObject *volatile items; pcount = PyMapping_Size(obj); + if (pcount < 0) + PLy_elog(ERROR, "could not get size of Python mapping"); + items = PyMapping_Items(obj); + if (items == NULL) + PLy_elog(ERROR, "could not get items from Python mapping"); PG_TRY(); { @@ -286,8 +291,15 @@ PLyMapping_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state) { JsonbValue jbvKey; PyObject *item = PyList_GetItem(items, i); - PyObject *key = PyTuple_GetItem(item, 0); - PyObject *value = PyTuple_GetItem(item, 1); + PyObject *key; + PyObject *value; + + /* The mapping's items() must yield key/value pairs */ + if (item == NULL || !PyTuple_Check(item) || PyTuple_Size(item) < 2) + PLy_elog(ERROR, "items() of a Python mapping must return key/value pairs"); + + key = PyTuple_GetItem(item, 0); + value = PyTuple_GetItem(item, 1); /* Python dictionary can have None as key */ if (key == Py_None) @@ -337,7 +349,10 @@ PLySequence_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state) for (i = 0; i < pcount; i++) { value = PySequence_GetItem(obj, i); - Assert(value); + + /* PySequence_GetItem() can return NULL, with an exception set */ + if (value == NULL) + PLy_elog(ERROR, "could not get element %d from sequence", (int) i); PLyObject_ToJsonbValue(value, jsonb_state, true); Py_XDECREF(value); diff --git a/contrib/jsonb_plpython/sql/jsonb_plpython.sql b/contrib/jsonb_plpython/sql/jsonb_plpython.sql index 29dc33279a092..fd8485c89c124 100644 --- a/contrib/jsonb_plpython/sql/jsonb_plpython.sql +++ b/contrib/jsonb_plpython/sql/jsonb_plpython.sql @@ -181,3 +181,80 @@ return x $$; SELECT test_dict1(); + +-- A custom sequence whose __getitem__ raises should be reported as an error, +-- not crash the backend +CREATE FUNCTION test_broken_sequence() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C: + def __len__(self): + return 2 + def __getitem__(self, i): + raise ValueError('getitem failed') +return C() +$$; + +SELECT test_broken_sequence(); + +-- A mapping whose items() raises should be reported as an error, not crash +-- the backend +CREATE FUNCTION test_broken_mapping() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C(dict): + def items(self): + raise ValueError('items failed') +d = C() +d['x'] = 1 +return d +$$; + +SELECT test_broken_mapping(); + +-- Likewise for a mapping whose items() does not return key/value pairs +CREATE FUNCTION test_malformed_mapping() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C(dict): + def items(self): + return [42] +d = C() +d['x'] = 1 +return d +$$; + +SELECT test_malformed_mapping(); + +-- Likewise for a mapping whose items() yields fewer pairs than its length +CREATE FUNCTION test_short_mapping() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C(dict): + def items(self): + return [] +d = C() +d['x'] = 1 +return d +$$; + +SELECT test_short_mapping(); + +-- Likewise for a mapping whose __len__() raises +CREATE FUNCTION test_broken_len_mapping() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C(dict): + def __len__(self): + raise ValueError('len failed') +d = C() +d['x'] = 1 +return d +$$; + +SELECT test_broken_len_mapping(); diff --git a/src/pl/plpython/expected/plpython_composite.out b/src/pl/plpython/expected/plpython_composite.out index 674af93ddcfe0..ffce7fc1be75b 100644 --- a/src/pl/plpython/expected/plpython_composite.out +++ b/src/pl/plpython/expected/plpython_composite.out @@ -606,3 +606,19 @@ DETAIL: Missing left parenthesis. HINT: To return a composite type in an array, return the composite type as a Python tuple, e.g., "[('foo',)]". CONTEXT: while creating return value PL/Python function "composite_type_as_list_broken" +-- A custom sequence whose length matches the tuple but whose __getitem__ +-- raises should be reported as an error, not crash the backend. +CREATE FUNCTION composite_type_as_broken_sequence() RETURNS type_record AS $$ +class C: + def __len__(self): + return 2 + def __getitem__(self, i): + raise ValueError('getitem failed') +return C() +$$ LANGUAGE plpython3u; +SELECT * FROM composite_type_as_broken_sequence(); +ERROR: could not get element 0 from sequence +DETAIL: ValueError: getitem failed +CONTEXT: Traceback (most recent call last): +while creating return value +PL/Python function "composite_type_as_broken_sequence" diff --git a/src/pl/plpython/expected/plpython_spi.out b/src/pl/plpython/expected/plpython_spi.out index b572f9bf73bcb..0320ff01f6b40 100644 --- a/src/pl/plpython/expected/plpython_spi.out +++ b/src/pl/plpython/expected/plpython_spi.out @@ -451,3 +451,54 @@ SELECT plan_composite_args(); (3,label) (1 row) +-- A custom argument sequence whose length matches the plan but whose +-- __getitem__ raises should be reported as an error, not crash the backend. +CREATE FUNCTION plan_broken_arg_sequence() RETURNS void AS $$ +plan = plpy.prepare("select $1", ["int4"]) +class C: + def __len__(self): + return 1 + def __getitem__(self, i): + raise ValueError('getitem failed') +plpy.execute(plan, C()) +$$ LANGUAGE plpython3u; +SELECT plan_broken_arg_sequence(); +ERROR: spiexceptions.ExternalRoutineException: could not get element 0 from sequence +DETAIL: ValueError: getitem failed +CONTEXT: Traceback (most recent call last): + PL/Python function "plan_broken_arg_sequence", line 8, in + plpy.execute(plan, C()) +PL/Python function "plan_broken_arg_sequence" +-- Likewise for the type-name list passed to plpy.prepare(). +CREATE FUNCTION prepare_broken_type_sequence() RETURNS void AS $$ +class C: + def __len__(self): + return 1 + def __getitem__(self, i): + raise ValueError('getitem failed') +plpy.prepare("select $1", C()) +$$ LANGUAGE plpython3u; +SELECT prepare_broken_type_sequence(); +ERROR: spiexceptions.ExternalRoutineException: could not get element 0 from sequence +DETAIL: ValueError: getitem failed +CONTEXT: Traceback (most recent call last): + PL/Python function "prepare_broken_type_sequence", line 7, in + plpy.prepare("select $1", C()) +PL/Python function "prepare_broken_type_sequence" +-- Likewise for the argument sequence passed to plpy.cursor(). +CREATE FUNCTION cursor_broken_arg_sequence() RETURNS void AS $$ +plan = plpy.prepare("select $1", ["int4"]) +class C: + def __len__(self): + return 1 + def __getitem__(self, i): + raise ValueError('getitem failed') +plpy.cursor(plan, C()) +$$ LANGUAGE plpython3u; +SELECT cursor_broken_arg_sequence(); +ERROR: spiexceptions.ExternalRoutineException: could not get element 0 from sequence +DETAIL: ValueError: getitem failed +CONTEXT: Traceback (most recent call last): + PL/Python function "cursor_broken_arg_sequence", line 8, in + plpy.cursor(plan, C()) +PL/Python function "cursor_broken_arg_sequence" diff --git a/src/pl/plpython/expected/plpython_types.out b/src/pl/plpython/expected/plpython_types.out index 8a680e15c14d0..0cb3d6ea8c688 100644 --- a/src/pl/plpython/expected/plpython_types.out +++ b/src/pl/plpython/expected/plpython_types.out @@ -796,6 +796,22 @@ SELECT * FROM test_type_conversion_array_error(); ERROR: return value of function with array return type is not a Python sequence CONTEXT: while creating return value PL/Python function "test_type_conversion_array_error" +-- A custom sequence whose __getitem__ raises should be reported as an error, +-- not crash the backend. +CREATE FUNCTION test_type_conversion_array_getitem_fail() RETURNS int[] AS $$ +class C: + def __len__(self): + return 2 + def __getitem__(self, i): + raise ValueError('getitem failed') +return C() +$$ LANGUAGE plpython3u; +SELECT * FROM test_type_conversion_array_getitem_fail(); +ERROR: could not get element 0 from sequence +DETAIL: ValueError: getitem failed +CONTEXT: Traceback (most recent call last): +while creating return value +PL/Python function "test_type_conversion_array_getitem_fail" -- -- Domains over arrays -- diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c index cc74c4df6ba67..0725fbc19f264 100644 --- a/src/pl/plpython/plpy_cursorobject.c +++ b/src/pl/plpython/plpy_cursorobject.c @@ -258,6 +258,11 @@ PLy_cursor_plan(PyObject *ob, PyObject *args) PyObject *elem; elem = PySequence_GetItem(args, j); + + /* PySequence_GetItem() can return NULL, with an exception set */ + if (elem == NULL) + PLy_elog(ERROR, "could not get element %d from sequence", j); + PG_TRY(2); { bool isnull; diff --git a/src/pl/plpython/plpy_spi.c b/src/pl/plpython/plpy_spi.c index 4ad40bf78f3da..4980873efcf22 100644 --- a/src/pl/plpython/plpy_spi.c +++ b/src/pl/plpython/plpy_spi.c @@ -87,6 +87,11 @@ PLy_spi_prepare(PyObject *self, PyObject *args) int32 typmod; optr = PySequence_GetItem(list, i); + + /* PySequence_GetItem() can return NULL, with an exception set */ + if (optr == NULL) + PLy_elog(ERROR, "could not get element %d from sequence", i); + if (PyUnicode_Check(optr)) sptr = PLyUnicode_AsString(optr); else @@ -250,6 +255,11 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit) PyObject *elem; elem = PySequence_GetItem(list, j); + + /* PySequence_GetItem() can return NULL, with an exception set */ + if (elem == NULL) + PLy_elog(ERROR, "could not get element %d from sequence", j); + PG_TRY(2); { bool isnull; diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c index 92d55bf9f423e..e499ed8cbfda2 100644 --- a/src/pl/plpython/plpy_typeio.c +++ b/src/pl/plpython/plpy_typeio.c @@ -1210,6 +1210,10 @@ PLySequence_ToArray_recurse(PyObject *obj, ArrayBuildState **astatep, /* fetch the array element */ PyObject *subobj = PySequence_GetItem(obj, i); + /* PySequence_GetItem() can return NULL, with an exception set */ + if (subobj == NULL) + PLy_elog(ERROR, "could not get element %d from sequence", i); + /* need PG_TRY to ensure we release the subobj's refcount */ PG_TRY(); { @@ -1456,7 +1460,10 @@ PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence) PG_TRY(); { value = PySequence_GetItem(sequence, idx); - Assert(value); + + /* PySequence_GetItem() can return NULL, with an exception set */ + if (value == NULL) + PLy_elog(ERROR, "could not get element %d from sequence", idx); values[i] = att->func(att, value, &nulls[i], false); diff --git a/src/pl/plpython/sql/plpython_composite.sql b/src/pl/plpython/sql/plpython_composite.sql index 1bb9b83b7191e..b401b3f2f6b93 100644 --- a/src/pl/plpython/sql/plpython_composite.sql +++ b/src/pl/plpython/sql/plpython_composite.sql @@ -233,3 +233,15 @@ CREATE FUNCTION composite_type_as_list_broken() RETURNS type_record[] AS $$ return [['first', 1]]; $$ LANGUAGE plpython3u; SELECT * FROM composite_type_as_list_broken(); + +-- A custom sequence whose length matches the tuple but whose __getitem__ +-- raises should be reported as an error, not crash the backend. +CREATE FUNCTION composite_type_as_broken_sequence() RETURNS type_record AS $$ +class C: + def __len__(self): + return 2 + def __getitem__(self, i): + raise ValueError('getitem failed') +return C() +$$ LANGUAGE plpython3u; +SELECT * FROM composite_type_as_broken_sequence(); diff --git a/src/pl/plpython/sql/plpython_spi.sql b/src/pl/plpython/sql/plpython_spi.sql index 00dcc8bb66937..276d130431e56 100644 --- a/src/pl/plpython/sql/plpython_spi.sql +++ b/src/pl/plpython/sql/plpython_spi.sql @@ -307,3 +307,42 @@ SELECT cursor_fetch_next_empty(); SELECT cursor_plan(); SELECT cursor_plan_wrong_args(); SELECT plan_composite_args(); + +-- A custom argument sequence whose length matches the plan but whose +-- __getitem__ raises should be reported as an error, not crash the backend. +CREATE FUNCTION plan_broken_arg_sequence() RETURNS void AS $$ +plan = plpy.prepare("select $1", ["int4"]) +class C: + def __len__(self): + return 1 + def __getitem__(self, i): + raise ValueError('getitem failed') +plpy.execute(plan, C()) +$$ LANGUAGE plpython3u; + +SELECT plan_broken_arg_sequence(); + +-- Likewise for the type-name list passed to plpy.prepare(). +CREATE FUNCTION prepare_broken_type_sequence() RETURNS void AS $$ +class C: + def __len__(self): + return 1 + def __getitem__(self, i): + raise ValueError('getitem failed') +plpy.prepare("select $1", C()) +$$ LANGUAGE plpython3u; + +SELECT prepare_broken_type_sequence(); + +-- Likewise for the argument sequence passed to plpy.cursor(). +CREATE FUNCTION cursor_broken_arg_sequence() RETURNS void AS $$ +plan = plpy.prepare("select $1", ["int4"]) +class C: + def __len__(self): + return 1 + def __getitem__(self, i): + raise ValueError('getitem failed') +plpy.cursor(plan, C()) +$$ LANGUAGE plpython3u; + +SELECT cursor_broken_arg_sequence(); diff --git a/src/pl/plpython/sql/plpython_types.sql b/src/pl/plpython/sql/plpython_types.sql index 0985a9cca2f44..31549c7f4f131 100644 --- a/src/pl/plpython/sql/plpython_types.sql +++ b/src/pl/plpython/sql/plpython_types.sql @@ -417,6 +417,19 @@ $$ LANGUAGE plpython3u; SELECT * FROM test_type_conversion_array_error(); +-- A custom sequence whose __getitem__ raises should be reported as an error, +-- not crash the backend. +CREATE FUNCTION test_type_conversion_array_getitem_fail() RETURNS int[] AS $$ +class C: + def __len__(self): + return 2 + def __getitem__(self, i): + raise ValueError('getitem failed') +return C() +$$ LANGUAGE plpython3u; + +SELECT * FROM test_type_conversion_array_getitem_fail(); + -- -- Domains over arrays From b7e4e3e7fa73458ecca5cd10f341743fd12a4faa Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 29 Jun 2026 13:15:07 +0900 Subject: [PATCH 13/36] doc: Reorder table for Object DDL Functions While on it, let's add links pointing to the set of SQL commands generated by these functions. The descriptions of the functions are exactly the same, just moved around. Author: Peter Smith Reviewed-by: Ian Lawrence Barwick Discussion: https://postgr.es/m/CAHut+Pun9Z8qZFJTa9fLgdhM=Cip9d-cnx2YXDW6eFrSwbQj1g@mail.gmail.com --- doc/src/sgml/func/func-info.sgml | 81 +++++++++++++++++--------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index bc80bcbd0b3cf..69ef3857cfa20 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -3874,6 +3874,34 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} + + + + pg_get_database_ddl + + pg_get_database_ddl + ( database regdatabase + , pretty boolean + DEFAULT false + , owner boolean + DEFAULT true + , tablespace boolean + DEFAULT true ) + setof text + + + Reconstructs the CREATE + DATABASE statement for the specified database, + followed by ALTER DATABASE + statements for connection limit, template status, + and configuration settings. Each statement is returned as a separate + row. When pretty is true, the output is + pretty-printed. When owner is false, the + OWNER clause is omitted. When + tablespace is false, the + TABLESPACE clause is omitted. + + @@ -3888,14 +3916,15 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} setof text - Reconstructs the CREATE ROLE statement and any - ALTER ROLE ... SET statements for the given role. - Each statement is returned as a separate row. + Reconstructs the CREATE ROLE + statement and any + ALTER ROLE ... SET statements for the given + role. Each statement is returned as a separate row. Password information is never included in the output. When pretty is true, the output is pretty-printed. When memberships is false, - GRANT statements for role memberships are - omitted. + GRANT statements + for role memberships are omitted. @@ -3921,40 +3950,14 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} setof text - Reconstructs the CREATE TABLESPACE statement for - the specified tablespace (by OID or name). If the tablespace has - options set, an ALTER TABLESPACE ... SET statement - is also returned. Each statement is returned as a separate row. - When pretty is true, the output is - pretty-printed. When owner is false, the - OWNER clause is omitted. - - - - - - pg_get_database_ddl - - pg_get_database_ddl - ( database regdatabase - , pretty boolean - DEFAULT false - , owner boolean - DEFAULT true - , tablespace boolean - DEFAULT true ) - setof text - - - Reconstructs the CREATE DATABASE statement for the - specified database, followed by ALTER DATABASE - statements for connection limit, template status, and configuration - settings. Each statement is returned as a separate row. - When pretty is true, the output is - pretty-printed. When owner is false, the - OWNER clause is omitted. When - tablespace is false, the - TABLESPACE clause is omitted. + Reconstructs the CREATE + TABLESPACE statement for the specified tablespace (by + OID or name). If the tablespace has options set, an + ALTER TABLESPACE ... SET + statement is also returned. Each statement is + returned as a separate row. When pretty is + true, the output is pretty-printed. When owner + is false, the OWNER clause is omitted. From 994f770a0fd55dfdeb96d1d60d35545ba2d51480 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 29 Jun 2026 11:49:11 +0200 Subject: [PATCH 14/36] Fix handling of copy_file_range() return value Treat copy_file_range() return value of zero as an error: it indicates that no bytes could be copied (perhaps the source file is shorter than expected), and the existing retry loop would otherwise spin forever since nwritten would never reach BLCKSZ. The other uses of copy_file_range() in the tree don't have this problem. Reviewed-by: Nazir Bilal Yavuz Reviewed-by: Kyotaro Horiguchi Reviewed-by: Yingying Chen Discussion: https://www.postgresql.org/message-id/flat/3208cf7a-c7f3-41eb-92f6-33cbeff4df40%40eisentraut.org --- src/bin/pg_combinebackup/reconstruct.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bin/pg_combinebackup/reconstruct.c b/src/bin/pg_combinebackup/reconstruct.c index 3349aa2441d29..38e756be7ef82 100644 --- a/src/bin/pg_combinebackup/reconstruct.c +++ b/src/bin/pg_combinebackup/reconstruct.c @@ -705,6 +705,9 @@ write_reconstructed_file(char *input_filename, if (wb < 0) pg_fatal("error while copying file range from \"%s\" to \"%s\": %m", input_filename, output_filename); + else if (wb == 0) + pg_fatal("unexpected end of file while copying file range from \"%s\" to \"%s\"", + input_filename, output_filename); nwritten += wb; From bc3ae886a759f2d3fd5f1b92f5fbeeccfee9e7a9 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 29 Jun 2026 15:13:45 +0200 Subject: [PATCH 15/36] Forbid FOR PORTION OF with WHERE CURRENT OF It is not clear how the implicit condition of FOR PORTION OF should interact with the use of a cursor. Normally, we forbid combining WHERE CURRENT OF with other WHERE conditions. The SQL standard only includes FOR PORTION OF with and , not or , so it is easy for us to exclude the functionality, at least for now. Author: Paul A. Jungwirth Discussion: https://www.postgresql.org/message-id/flat/CA%2BrenyUEKPexUYsH4qeU8_o1jqKsUkEWca1keS6n21shgG1g%2BA%40mail.gmail.com --- doc/src/sgml/ref/delete.sgml | 7 ++-- doc/src/sgml/ref/update.sgml | 7 ++-- src/backend/parser/analyze.c | 10 +++++ src/test/regress/expected/for_portion_of.out | 42 ++++++++++++++++++++ src/test/regress/sql/for_portion_of.sql | 28 +++++++++++++ 5 files changed, 86 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml index 9066d7ea83df8..ffdcd7fc4fafa 100644 --- a/doc/src/sgml/ref/delete.sgml +++ b/doc/src/sgml/ref/delete.sgml @@ -231,10 +231,9 @@ DELETE FROM [ ONLY ] table_name [ * from this cursor. The cursor must be a non-grouping query on the DELETE's target table. Note that WHERE CURRENT OF cannot be - specified together with a Boolean condition. See - - for more information about using cursors with - WHERE CURRENT OF. + specified together with a Boolean condition or FOR PORTION + OF. See for more information + about using cursors with WHERE CURRENT OF. diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml index dd57bead90cdb..21a8fd8b0374a 100644 --- a/doc/src/sgml/ref/update.sgml +++ b/doc/src/sgml/ref/update.sgml @@ -287,10 +287,9 @@ UPDATE [ ONLY ] table_name [ * ] from this cursor. The cursor must be a non-grouping query on the UPDATE's target table. Note that WHERE CURRENT OF cannot be - specified together with a Boolean condition. See - - for more information about using cursors with - WHERE CURRENT OF. + specified together with a Boolean condition or FOR PORTION + OF. See for more information + about using cursors with WHERE CURRENT OF. diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index dc65a505c1657..2932d17a107b8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -81,6 +81,7 @@ static OnConflictExpr *transformOnConflictClause(ParseState *pstate, static ForPortionOfExpr *transformForPortionOfClause(ParseState *pstate, int rtindex, const ForPortionOfClause *forPortionOf, + const Node *whereClause, bool isUpdate); static int count_rowexpr_columns(ParseState *pstate, Node *expr); static Query *transformSelectStmt(ParseState *pstate, SelectStmt *stmt, @@ -626,6 +627,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf, + stmt->whereClause, false); qual = transformWhereClause(pstate, stmt->whereClause, @@ -1319,6 +1321,7 @@ static ForPortionOfExpr * transformForPortionOfClause(ParseState *pstate, int rtindex, const ForPortionOfClause *forPortionOf, + const Node *whereClause, bool isUpdate) { Relation targetrel = pstate->p_target_relation; @@ -1335,6 +1338,12 @@ transformForPortionOfClause(ParseState *pstate, ForPortionOfExpr *result; Var *rangeVar; + /* disallow FOR PORTION OF ... WHERE CURRENT OF */ + if (whereClause && IsA(whereClause, CurrentOfExpr)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("WHERE CURRENT OF with FOR PORTION OF is not implemented")); + result = makeNode(ForPortionOfExpr); /* Look up the FOR PORTION OF name requested. */ @@ -2875,6 +2884,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf, + stmt->whereClause, true); nsitem = pstate->p_target_nsitem; diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out index 4340897211771..207e370627e2d 100644 --- a/src/test/regress/expected/for_portion_of.out +++ b/src/test/regress/expected/for_portion_of.out @@ -2446,4 +2446,46 @@ NOTICE: fpo_before_row1: BEFORE UPDATE ROW: NOTICE: old: [10,100) NOTICE: new: [30,70) DROP TABLE fpo_update_of_trigger; +-- CURSORs +CREATE TABLE fpo_cursed ( + id int, + valid_at int4range +); +INSERT INTO fpo_cursed (id, valid_at) VALUES (1, '[10,100)'); +-- UPDATE FOR PORTION OF is not permitted with a CURSOR: +BEGIN; +DECLARE fpo_cur CURSOR FOR SELECT * FROM fpo_cursed; +FETCH NEXT FROM fpo_cur; + id | valid_at +----+---------- + 1 | [10,100) +(1 row) + +UPDATE fpo_cursed + FOR PORTION OF valid_at FROM 5 TO 6 + SET id = 2 + WHERE CURRENT OF fpo_cur; +ERROR: WHERE CURRENT OF with FOR PORTION OF is not implemented +ROLLBACK; +-- DELETE FOR PORTION OF is not permitted with a CURSOR: +BEGIN; +DECLARE fpo_cur CURSOR FOR SELECT * FROM fpo_cursed; +FETCH NEXT FROM fpo_cur; + id | valid_at +----+---------- + 1 | [10,100) +(1 row) + +DELETE FROM fpo_cursed + FOR PORTION OF valid_at FROM 8 TO 9 + WHERE CURRENT OF fpo_cur; +ERROR: WHERE CURRENT OF with FOR PORTION OF is not implemented +ROLLBACK; +SELECT * FROM fpo_cursed; + id | valid_at +----+---------- + 1 | [10,100) +(1 row) + +DROP TABLE fpo_cursed; RESET datestyle; diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql index 7b08f8cf45e14..a3c41abf7b7dd 100644 --- a/src/test/regress/sql/for_portion_of.sql +++ b/src/test/regress/sql/for_portion_of.sql @@ -1591,4 +1591,32 @@ UPDATE fpo_update_of_trigger SET id = 2; DROP TABLE fpo_update_of_trigger; +-- CURSORs +CREATE TABLE fpo_cursed ( + id int, + valid_at int4range +); +INSERT INTO fpo_cursed (id, valid_at) VALUES (1, '[10,100)'); + +-- UPDATE FOR PORTION OF is not permitted with a CURSOR: +BEGIN; +DECLARE fpo_cur CURSOR FOR SELECT * FROM fpo_cursed; +FETCH NEXT FROM fpo_cur; +UPDATE fpo_cursed + FOR PORTION OF valid_at FROM 5 TO 6 + SET id = 2 + WHERE CURRENT OF fpo_cur; +ROLLBACK; + +-- DELETE FOR PORTION OF is not permitted with a CURSOR: +BEGIN; +DECLARE fpo_cur CURSOR FOR SELECT * FROM fpo_cursed; +FETCH NEXT FROM fpo_cur; +DELETE FROM fpo_cursed + FOR PORTION OF valid_at FROM 8 TO 9 + WHERE CURRENT OF fpo_cur; +ROLLBACK; +SELECT * FROM fpo_cursed; +DROP TABLE fpo_cursed; + RESET datestyle; From 52e118fe2f7e3381bdaa479816a7f72eda2ae517 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 29 Jun 2026 16:15:13 +0200 Subject: [PATCH 16/36] Fix typo from commit c1fe2d1a383 --- src/bin/pg_upgrade/check.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 7556fb3f22a35..f8f31382835c8 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -2635,7 +2635,7 @@ check_old_cluster_global_names(ClusterInfo *cluster) { fclose(script); pg_log(PG_REPORT, "fatal"); - pg_fatal("Your installation contains databases, roles, or tablespace with names\n" + pg_fatal("Your installation contains databases, roles, or tablespaces with names\n" "with invalid characters (newline or carriage return). To fix this,\n" "rename these objects.\n" "A list of all objects with invalid names is in the file:\n" From 3f815dd11374b54deb29228dc0040179864af828 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 29 Jun 2026 15:16:25 -0400 Subject: [PATCH 17/36] Sync typedefs.list with the buildfarm. Replace typedefs.list with the authoritative list from our buildfarm, and run pgindent using that. --- src/tools/pgindent/typedefs.list | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index c5db6ca67051c..3a2720fb5f988 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -525,6 +525,7 @@ ConnType ConnectionStateEnum ConnectionTiming ConnectionWarning +ConnectionWarningFilter ConsiderSplitContext Const ConstrCheck @@ -723,7 +724,6 @@ ENGINE EOM_flatten_into_method EOM_get_flat_size_method EPQState -EPlan EState EStatus EVP_CIPHER @@ -3110,7 +3110,6 @@ TParserStateActionItem TQueueDestReceiver TRGM TSAnyCacheEntry -TscClockSourceInfo TSConfigCacheEntry TSConfigInfo TSDictInfo @@ -3261,6 +3260,7 @@ TriggerInfo TriggerInstrumentation TriggerTransition TruncateStmt +TscClockSourceInfo TsmRoutine TupOutputState TupSortStatus From 99e44c3181c779ae0f3539ba7f408661983fbf8e Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 29 Jun 2026 15:27:44 -0400 Subject: [PATCH 18/36] Run pgperltidy This is required before the creation of a new branch. pgindent is clean, as well as is reformat-dat-files. perltidy version is v20230309, as documented in pgindent's README. --- contrib/dblink/t/001_auth_scram.pl | 13 ++++---- contrib/postgres_fdw/t/001_auth_scram.pl | 20 +++++++------ src/include/catalog/pg_proc.dat | 30 +++++++++---------- src/test/modules/test_misc/t/012_ddlutils.pl | 5 +++- .../recovery/t/051_effective_wal_level.pl | 3 +- src/test/ssl/t/001_ssltests.pl | 9 ++++-- src/test/subscription/t/100_bugs.pl | 6 ++-- 7 files changed, 46 insertions(+), 40 deletions(-) diff --git a/contrib/dblink/t/001_auth_scram.pl b/contrib/dblink/t/001_auth_scram.pl index b087b38e5a58a..8d6280656d51e 100644 --- a/contrib/dblink/t/001_auth_scram.pl +++ b/contrib/dblink/t/001_auth_scram.pl @@ -103,10 +103,10 @@ { my $connstr = $node1->connstr($db0) . qq' user=$user'; - $node1->safe_psql($db0, + $node1->safe_psql( + $db0, qq'ALTER USER MAPPING FOR $user SERVER $fdw_server3 OPTIONS(add use_scram_passthrough \'false\')', - connstr => $connstr - ); + connstr => $connstr); my ($ret, $stdout, $stderr) = $node1->psql( $db0, @@ -114,10 +114,9 @@ connstr => $connstr); is($ret, 3, 'SCRAM passthrough disabled on user mapping should fail'); - like( - $stderr, - qr/password/i, - 'expected password-related error when scram passthrough disabled on user mapping'); + like($stderr, qr/password/i, + 'expected password-related error when scram passthrough disabled on user mapping' + ); } # Ensure that trust connections fail without superuser opt-in. diff --git a/contrib/postgres_fdw/t/001_auth_scram.pl b/contrib/postgres_fdw/t/001_auth_scram.pl index c4b57cd81b38d..972faf1f5529a 100644 --- a/contrib/postgres_fdw/t/001_auth_scram.pl +++ b/contrib/postgres_fdw/t/001_auth_scram.pl @@ -75,16 +75,19 @@ { my $connstr = $node1->connstr($db0) . qq' user=$user'; - $node1->safe_psql($db0, + $node1->safe_psql( + $db0, qq'ALTER USER MAPPING FOR $user SERVER $fdw_server3 OPTIONS(add use_scram_passthrough \'false\')', - connstr => $connstr - ); + connstr => $connstr); $node1->safe_psql( $db0, qq'CREATE FOREIGN TABLE override_t (g int, col2 int) SERVER $fdw_server3 OPTIONS (table_name \'t\');', - connstr => $connstr ); - $node1->safe_psql($db0, qq'GRANT SELECT ON override_t TO $user;', connstr => $connstr); + connstr => $connstr); + $node1->safe_psql( + $db0, + qq'GRANT SELECT ON override_t TO $user;', + connstr => $connstr); my ($ret, $stdout, $stderr) = $node1->psql( $db0, @@ -92,10 +95,9 @@ connstr => $connstr); is($ret, 3, 'SCRAM passthrough disabled on user mapping should fail'); - like( - $stderr, - qr/password/i, - 'expected password-related error when scram passthrough disabled on user mapping'); + like($stderr, qr/password/i, + 'expected password-related error when scram passthrough disabled on user mapping' + ); } SKIP: diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 402d869710bd7..1a985becde32d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8591,27 +8591,25 @@ proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' }, { oid => '6501', descr => 'get DDL to recreate a role', - proname => 'pg_get_role_ddl', prorows => '10', proisstrict => 't', - proretset => 't', provolatile => 's', pronargdefaults => '2', - prorettype => 'text', proargtypes => 'regrole bool bool', - proargnames => '{role,pretty,memberships}', - proargdefaults => '{false,true}', prosrc => 'pg_get_role_ddl' }, + proname => 'pg_get_role_ddl', prorows => '10', proretset => 't', + provolatile => 's', pronargdefaults => '2', prorettype => 'text', + proargtypes => 'regrole bool bool', + proargnames => '{role,pretty,memberships}', proargdefaults => '{false,true}', + prosrc => 'pg_get_role_ddl' }, { oid => '6499', descr => 'get DDL to recreate a tablespace', - proname => 'pg_get_tablespace_ddl', prorows => '10', proisstrict => 't', - proretset => 't', provolatile => 's', pronargdefaults => '2', - prorettype => 'text', proargtypes => 'oid bool bool', - proargnames => '{tablespace,pretty,owner}', + proname => 'pg_get_tablespace_ddl', prorows => '10', proretset => 't', + provolatile => 's', pronargdefaults => '2', prorettype => 'text', + proargtypes => 'oid bool bool', proargnames => '{tablespace,pretty,owner}', proargdefaults => '{false,true}', prosrc => 'pg_get_tablespace_ddl_oid' }, { oid => '6500', descr => 'get DDL to recreate a tablespace', - proname => 'pg_get_tablespace_ddl', prorows => '10', proisstrict => 't', - proretset => 't', provolatile => 's', pronargdefaults => '2', - prorettype => 'text', proargtypes => 'name bool bool', - proargnames => '{tablespace,pretty,owner}', + proname => 'pg_get_tablespace_ddl', prorows => '10', proretset => 't', + provolatile => 's', pronargdefaults => '2', prorettype => 'text', + proargtypes => 'name bool bool', proargnames => '{tablespace,pretty,owner}', proargdefaults => '{false,true}', prosrc => 'pg_get_tablespace_ddl_name' }, { oid => '6502', descr => 'get DDL to recreate a database', - proname => 'pg_get_database_ddl', prorows => '10', proisstrict => 't', - proretset => 't', provolatile => 's', pronargdefaults => '3', - prorettype => 'text', proargtypes => 'regdatabase bool bool bool', + proname => 'pg_get_database_ddl', prorows => '10', proretset => 't', + provolatile => 's', pronargdefaults => '3', prorettype => 'text', + proargtypes => 'regdatabase bool bool bool', proargnames => '{database,pretty,owner,tablespace}', proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' }, { oid => '2509', diff --git a/src/test/modules/test_misc/t/012_ddlutils.pl b/src/test/modules/test_misc/t/012_ddlutils.pl index e541dfa38d12f..c4096879922e1 100644 --- a/src/test/modules/test_misc/t/012_ddlutils.pl +++ b/src/test/modules/test_misc/t/012_ddlutils.pl @@ -181,7 +181,10 @@ sub ddl_filter q{SELECT * FROM pg_get_database_ddl('regression_ddlutils_test', owner => 'invalid')} ); isnt($ret, 0, 'invalid boolean option errors'); -like($stderr, qr/invalid input syntax for type boolean/, 'invalid option error message'); +like( + $stderr, + qr/invalid input syntax for type boolean/, + 'invalid option error message'); # Duplicate named argument ($ret, $stdout, $stderr) = $node->psql( diff --git a/src/test/recovery/t/051_effective_wal_level.pl b/src/test/recovery/t/051_effective_wal_level.pl index c45eddc7383a7..d4bc7f0aa407c 100644 --- a/src/test/recovery/t/051_effective_wal_level.pl +++ b/src/test/recovery/t/051_effective_wal_level.pl @@ -76,8 +76,7 @@ sub wait_for_logical_decoding_disabled # Wait for the checkpointer to disable logical decoding. wait_for_logical_decoding_disabled($primary); test_wal_level($primary, "replica|replica", - "logical decoding disabled after repack" -); + "logical decoding disabled after repack"); # Create a new logical slot and check that effective_wal_level must be increased # to 'logical'. diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 01f3573e1fd0c..cb7c2a061934f 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -885,7 +885,8 @@ sub switch_server_cert "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'), "certificate authorization fails with revoked client cert", - expected_stderr => qr!SSL error: (ssl[a-z0-9/]*|tls) alert certificate revoked!, + expected_stderr => + qr!SSL error: (ssl[a-z0-9/]*|tls) alert certificate revoked!, log_like => [ qr{Client certificate verification failed at depth 0: certificate revoked}, qr{Failed certificate data \(unverified\): subject "/CN=ssltestuser", serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"}, @@ -987,7 +988,8 @@ sub switch_server_cert "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'), "certificate authorization fails with revoked client cert with server-side CRL directory", - expected_stderr => qr!SSL error: (ssl[a-z0-9/]*|tls) alert certificate revoked!, + expected_stderr => + qr!SSL error: (ssl[a-z0-9/]*|tls) alert certificate revoked!, log_like => [ qr{Client certificate verification failed at depth 0: certificate revoked}, qr{Failed certificate data \(unverified\): subject "/CN=ssltestuser", serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"}, @@ -998,7 +1000,8 @@ sub switch_server_cert "$common_connstr user=ssltestuser sslcert=ssl/client-revoked-utf8.crt " . sslkey('client-revoked-utf8.key'), "certificate authorization fails with revoked UTF-8 client cert with server-side CRL directory", - expected_stderr => qr!SSL error: (ssl[a-z0-9/]*|tls) alert certificate revoked!, + expected_stderr => + qr!SSL error: (ssl[a-z0-9/]*|tls) alert certificate revoked!, log_like => [ qr{Client certificate verification failed at depth 0: certificate revoked}, qr{Failed certificate data \(unverified\): subject "/CN=\\xce\\x9f\\xce\\xb4\\xcf\\x85\\xcf\\x83\\xcf\\x83\\xce\\xad\\xce\\xb1\\xcf\\x82", serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"}, diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 31dc63ae8c413..075c52f98fd2e 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -644,8 +644,10 @@ BEGIN ); # Clean up -$node_publisher->safe_psql('postgres', "SELECT pg_drop_replication_slot('upsert_slot')"); -$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub_rowfilter_error"); +$node_publisher->safe_psql('postgres', + "SELECT pg_drop_replication_slot('upsert_slot')"); +$node_publisher->safe_psql('postgres', + "DROP PUBLICATION pub_rowfilter_error"); $node_publisher->safe_psql('postgres', "DROP TABLE tab_upsert"); $node_publisher->stop('fast'); From 9cfd19bc10ac07139ca6c6d051d4492764441edb Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 29 Jun 2026 15:33:52 -0400 Subject: [PATCH 19/36] Add previous 2 commits to .git-blame-ignore-revs. --- .git-blame-ignore-revs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f9a955af12da1..4d8f98d953b15 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -14,6 +14,12 @@ # # $ git log --pretty=format:"%H # %cd%n# %s" $PGINDENTGITHASH -1 --date=iso +99e44c3181c779ae0f3539ba7f408661983fbf8e # 2026-06-29 15:27:44 -0400 +# Run pgperltidy + +3f815dd11374b54deb29228dc0040179864af828 # 2026-06-29 15:16:25 -0400 +# Sync typedefs.list with the buildfarm. + bd57abbb1910e51e45761b59985745d094ae9e03 # 2026-06-04 10:15:37 -0500 # Re-pgindent nodeModifyTable.c after commit 993a7aa0e4. From a281a3e6dbb45d5cca41ea5f7b746724eb3ca70d Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 29 Jun 2026 16:29:11 -0400 Subject: [PATCH 20/36] Stamp HEAD as 20devel. Let the hacking begin ... --- configure | 18 +- configure.ac | 2 +- doc/src/sgml/filelist.sgml | 2 +- doc/src/sgml/release-19.sgml | 3330 ---------------------------------- doc/src/sgml/release-20.sgml | 16 + doc/src/sgml/release.sgml | 2 +- meson.build | 2 +- src/tools/git_changelog | 2 +- src/tools/version_stamp.pl | 2 +- 9 files changed, 31 insertions(+), 3345 deletions(-) delete mode 100644 doc/src/sgml/release-19.sgml create mode 100644 doc/src/sgml/release-20.sgml diff --git a/configure b/configure index 5f77f3cac29f3..35b0b72f0a70f 100755 --- a/configure +++ b/configure @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.69 for PostgreSQL 19beta1. +# Generated by GNU Autoconf 2.69 for PostgreSQL 20devel. # # Report bugs to . # @@ -582,8 +582,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='PostgreSQL' PACKAGE_TARNAME='postgresql' -PACKAGE_VERSION='19beta1' -PACKAGE_STRING='PostgreSQL 19beta1' +PACKAGE_VERSION='20devel' +PACKAGE_STRING='PostgreSQL 20devel' PACKAGE_BUGREPORT='pgsql-bugs@lists.postgresql.org' PACKAGE_URL='https://www.postgresql.org/' @@ -1468,7 +1468,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures PostgreSQL 19beta1 to adapt to many kinds of systems. +\`configure' configures PostgreSQL 20devel to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1533,7 +1533,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of PostgreSQL 19beta1:";; + short | recursive ) echo "Configuration of PostgreSQL 20devel:";; esac cat <<\_ACEOF @@ -1724,7 +1724,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -PostgreSQL configure 19beta1 +PostgreSQL configure 20devel generated by GNU Autoconf 2.69 Copyright (C) 2012 Free Software Foundation, Inc. @@ -2477,7 +2477,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by PostgreSQL $as_me 19beta1, which was +It was created by PostgreSQL $as_me 20devel, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ @@ -20348,7 +20348,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by PostgreSQL $as_me 19beta1, which was +This file was extended by PostgreSQL $as_me 20devel, which was generated by GNU Autoconf 2.69. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -20419,7 +20419,7 @@ _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" ac_cs_version="\\ -PostgreSQL config.status 19beta1 +PostgreSQL config.status 20devel configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" diff --git a/configure.ac b/configure.ac index 61cee42daa721..0e624fe36b96d 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Read the Autoconf manual for details. dnl m4_pattern_forbid(^PGAC_)dnl to catch undefined macros -AC_INIT([PostgreSQL], [19beta1], [pgsql-bugs@lists.postgresql.org], [], [https://www.postgresql.org/]) +AC_INIT([PostgreSQL], [20devel], [pgsql-bugs@lists.postgresql.org], [], [https://www.postgresql.org/]) m4_if(m4_defn([m4_PACKAGE_VERSION]), [2.69], [], [m4_fatal([Autoconf version 2.69 is required. Untested combinations of 'autoconf' and PostgreSQL versions are not diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index 25a85082759b4..0e7e7c8ca5377 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -185,7 +185,7 @@ - + diff --git a/doc/src/sgml/release-19.sgml b/doc/src/sgml/release-19.sgml deleted file mode 100644 index b0925d5f24bbd..0000000000000 --- a/doc/src/sgml/release-19.sgml +++ /dev/null @@ -1,3330 +0,0 @@ - - - - - Release 19 - - - Release date: - 2026-??-??, AS OF 2026-06-17 - - - - Overview - - - PostgreSQL 19 contains many new features - and enhancements, including: - - - - - fill in later - - - - - The above items and other new features of - PostgreSQL 19 are explained in more detail - in the sections below. - - - - - - - Migration to Version 19 - - - A dump/restore using or use of - or logical replication is required for - those wishing to migrate data from any previous release. See for general information on migrating to new - major releases. - - - - Version 19 contains a number of changes that may affect compatibility - with previous releases. Observe the following incompatibilities: - - - - - - - -Add server variable password_expiration_warning_threshold to warn about password expiration (Gilles Darold, Nathan Bossart) -§ - - - -The default warning period is seven days. - - - - - - - -Issue a warning after successful MD5 password authentication (Nathan Bossart) -§ - - - -The warning can be disabled via server variable md5_password_warnings. MD5 passwords were marked as deprecated in PostgreSQL 18. - - - - - - - -Remove RADIUS support (Thomas Munro) -§ - - - -PostgreSQL only supported RADIUS over UDP, which is unfixably insecure. - - - - - - - -Force standard_conforming_strings to always be on in the database server (Tom Lane) -§ - - - -Dumps created using pre-PostgreSQL 19 versions of pg_dump -or pg_dumpall, and using standard_conforming_strings = off, -will not properly load into PostgreSQL 19 and later servers. Users should create dumps using PostgreSQL 19 -or later versions of these applications, or use standard_conforming_strings = on. - - - -Client applications still support operations with servers having standard_conforming_strings = off, for compatibility with old servers. The server variable escape_string_warning has been removed as unnecessary. - - - - - - - -Disallow carriage returns and line feeds in database, role, and tablespace names (Mahendra Singh Thalor) -§ - - - -pg_upgrade will also disallow upgrading of clusters that use such names. This was changed to avoid security problems. - - - - - - - -Change the default index opclasses for inet and cidr data types from to GiST (Tom Lane) -§ -§ - - - -The inet/cidr opclasses are broken because they can exclude rows that should be returned. pg_upgrade will disallow upgrading of clusters with inet/cidr indexes. - - - - - - - -Stop reordering non-schema objects created by CREATE SCHEMA (Tom Lane, Jian He) -§ -§ - - - -The goal of the reordering was to avoid dependencies, but it was imperfect. PostgreSQL now uses the specified object ordering, except for foreign keys which are created last. - - - - - - - -Disallow system columns from being used in COPY FROM ... WHERE (Tom Lane) -§ - - - -The values of such columns were not well-defined. - - - - - - - -Change a json_array() call which returns no rows to return an empty JSON array (Richard Guo) -§ - - - -This previously returned NULL. - - - - - - - -Cause transactions to pass their READ ONLY and DEFERRABLE status to sessions (Etsuro Fujita) -§ - - - -This means READ ONLY transactions can no longer modify rows processed by sessions. - - - - - - - -Change default of max_locks_per_transaction from 64 to 128 (Heikki Linnakangas) -§ - - - -Lock size allocation has changed, so effectively settings must now be doubled to match their capacity in previous releases. - - - - - - - -Change JIT to be disabled by default (Jelte Fennema-Nio) -§ - - - -Previously JIT was enabled by default, and activated based on optimizer costs, but this costing has been determined to be unreliable. This change requires sites that are doing many -large analytical queries to manually enable JIT. - - - - - - - -Rename column sync_error_count to sync_table_error_count in system view pg_stat_subscription_stats (Vignesh C) -§ - - - -This is necessary since sequence errors are now tracked separately. - - - - - - - -Rename wait event type BUFFERPIN to BUFFER (Andres Freund) -§ - - - - - - - -Change index access method handlers to use a static IndexAmRoutines structure, rather than dynamically allocated ones (Matthias van de Meent) -§ - - - - - - - -Remove optimizer hook get_relation_info_hook and add better-placed hook build_simple_rel_hook (Robert Haas) -§ - - - - - - - -Remove MULE_INTERNAL encoding (Thomas Munro) -§ - - - -This encoding was complex and rarely used. Databases using it will need to be dumped and restored with a different encoding. - - - - - - - - - Changes - - - Below you will find a detailed account of the changes between - PostgreSQL 19 and the previous major - release. - - - - Server - - - Optimizer - - - - - - -Allow NOT IN clauses to be converted to more efficient ANTI JOINs when NULLs are not present (Richard Guo) -§ - - - - - - - -Allow more LEFT JOINs to be converted to ANTI JOINs (Tender Wang, Richard Guo) -§ - - - - - - - -Allow use of Memoize for ANTI JOINs with unique inner sides (Richard Guo) -§ - - - - - - - -Allow some aggregate processing to be performed before joins (Richard Guo, Antonin Houska) -§ -§ -§ - - - -This can reduce the number of rows needed to be processed. - - - - - - - -Improve hash join's handling of tuples with NULL join keys (Tom Lane) -§ - - - - - - - -Improve the planning of semijoins (Richard Guo) -§ - - - - - - - -Allow Append and MergeAppend to consider explicit incremental sorts (Richard Guo) -§ - - - - - - - -Convert IS [NOT] DISTINCT FROM NULL to IS [NOT] NULL during constant folding (Richard Guo) -§ - - - -The latter form is more easily optimized. - - - - - - - -Simplify IS [NOT] DISTINCT FROM to equality/inequality operators when inputs are proven non-nullable (Richard Guo) -§ - - - - - - - -Perform earlier constant folding of var IS [NOT] NULL in the optimizer (Richard Guo) -§ - - - -This allows for later optimizations. - - - - - - - -Simplify COALESCE() and ROW(...) IS [NOT] NULL to avoid evaluating unnecessary arguments (Richard Guo) -§ -§ - - - - - - - -Simplify IS [NOT] TRUE/FALSE/UNKNOWN to plain boolean expressions when the input is proven non-nullable (Richard Guo) -§ - - - - - - - -Speed up join selectivity computations for large optimizer statistics targets (Ilia Evdokimov, David Geier) -§ - - - - - - - -Enable proper optimizer statistics for functions returning boolean values (Tom Lane) -§ - - - - - - - -Allow extended statistics on virtual generated columns (Yugo Nagata) -§ - - - - - - - -Allow function pg_restore_extended_stats() to restore optimizer extended statistics (Corey Huinker, Michael Paquier, Chao Li) -§ -§ -§ -§ - - - - - - - -Add function pg_clear_extended_stats() to remove extended statistics (Corey Huinker, Michael Paquier) -§ - - - - - - - -Adjust the optimizer to consider startup costs of partial paths (Robert Haas, Tomas Vondra) -§ - - - - - - - -Allow negative values of pg_aggregate.aggtransspace to indicate unbounded memory usage (Richard Guo) -§ - - - -This information is used by the optimizer in planning memory usage. - - - - - - - - - General Performance - - - - - - -Improve performance of foreign key constraint checks (Junwang Zhao, Amit Langote, Chao Li) -§ -§ -§ -§ - - - - - - - -Improve asynchronous I/O read-ahead scheduling for large requests (Andres Freund) -§ -§ -§ - - - - - - - -Allow io_method method worker to automatically control needed background workers (Thomas Munro) -§ - - - -The new server variables are io_min_workers, io_max_workers, io_worker_idle_timeout, and io_worker_launch_interval. - - - - - - - -Allow query table scans to mark pages as all-visible in the visibility map (Melanie Plageman) -§ - - - -Previously only VACUUM and COPY ... FREEZE could do this. - - - - - - - -Allow autovacuum to use parallel autovacuum workers (Daniil Davydov) -§ -§ - - - -The maximum number of workers is controlled by server variable autovacuum_max_parallel_workers and per-table storage parameter autovacuum_parallel_workers. - - - - - - - -Allow TID Range Scans to be parallelized (Cary Huang, David Rowley) -§ - - - - - - - -Improve COPY FROM performance for text and CSV input using SIMD CPU instructions (Nazir Bilal Yavuz, Shinya Kato) -§ - - - - - - - -Improve NOTIFY to only wake up backends that are listening to specified notifications (Joel Jacobson) -§ - - - -Previously most backends were woken by NOTIFY. - - - - - - - -Change the default TOAST compression method from pglz to the more efficient lz4 (Euler Taveira) -§ - - - -This is done by changing the default for server variable default_toast_compression. - - - - - - - -Improve performance of internal row deformation (David Rowley) -§ - - - - - - - -Improve performance of repeated UTF-8 case-folding operations (Andreas Karlsson) -§ - - - - - - - -Improve performance of hash index bulk-deletion and GIN index vacuuming using streaming reads (Xuneng Zhou) -§ -§ - - - - - - - -Improve sort performance using radix sort (John Naylor) -§ - - - - - - - -Improve timing performance measurements (Lukas Fittl, Andres Freund, David Geier) -§ -§ - - - -This benefits EXPLAIN (ANALYZE, TIMING) and pg_test_timing, and is controlled via server variable timing_clock_source. - - - - - - - -Optimize plpgsql syntax SELECT simple-expression INTO var (Tom Lane) -§ - - - - - - - - - System Views - - - - - - -Add system view pg_stat_lock and function pg_stat_get_lock() to report per-lock-type statistics (Bertrand Drouvot) -§ - - - - - - - -Add system view pg_stat_recovery to report recovery status (Xuneng Zhou, Shinya Kato) -§ -§ - - - - - - - -Add system view pg_stat_autovacuum_scores to report per-table autovacuum details (Sami Imseih) -§ - - - - - - - -Add system view pg_dsm_registry_allocations to report dynamic shared memory details (Florents Tselai, Nathan Bossart) -§ -§ - - - - - - - -Add vacuum initiation details to system view pg_stat_progress_vacuum (Shinya Kato) -§ - - - -The new started_by column reports the initiator of the vacuum, and mode indicates its aggressiveness. - - - - - - - -Add analyze initiation details to system view pg_stat_progress_analyze (Shinya Kato) -§ - - - -The new started_by column reports the initiator of the analyze. - - - - - - - -Add mem_exceeded_count column to system view pg_stat_replication_slots (Bertrand Drouvot) -§ - - - -This reports the number of times that logical_decoding_work_mem was exceeded. - - - - - - - -Add slot synchronization skip information to pg_stat_replication_slots and pg_replication_slots (Shlok Kyal) -§ -§ -§ - - - -The new columns are slotsync_skip_count, slotsync_last_skip, and slotsync_skip_reason. - - - - - - - -Add update_deleted column to system view pg_stat_subscription_stats (Zhijie Hou) -§ - - - -This reports the number of rows where updates were ignored due to concurrent deletes. This requires the subscriber have retain_dead_tuples enabled. - - - - - - - -Add sync_seq_error_count column to system view pg_stat_subscription_stats to report sequence synchronization errors (Vignesh C) -§ -§ - - - - - - - -Add stats_reset column to system views pg_stat_all_tables, pg_stat_all_indexes, and pg_statio_all_sequences (Bertrand Drouvot, Sami Imseih, Shihao Zhong) -§ - - - -It also appears in the sys and user view variants. - - - - - - - -Add stats_reset column to system views pg_stat_user_functions and pg_stat_database_conflicts (Bertrand Drouvot, Shihao Zhong) -§ -§ - - - - - - - -Add location column to system views pg_available_extensions and pg_available_extension_versions to report the file system directory of extensions (Matheus Alcantara) -§ - - - - - - - -Add backup_type column to system view pg_stat_progress_basebackup to report the type of backup (Shinya Kato) -§ - - - -Possible values are full or incremental. - - - - - - - -Add connecting value to system view column pg_stat_wal_receiver.status (Xuneng Zhou) -§ - - - - - - - -Add reporting of the bytes written to WAL for full page images (Shinya Kato) -§ - - - -This is accessible via system view pg_stat_wal and function pg_stat_get_backend_wal(). - - - - - - - -Add columns to system views pg_stats, pg_stats_ext, and pg_stats_ext_exprs (Corey Huinker) -§ - - - -Adds table OID and attribute number columns to pg_stats, and table OID and statistics object OID columns to the other two. - - - - - - - -Add information about range type extended statistics to system view pg_stats_ext_exprs (Corey Huinker, Michael Paquier) -§ - - - - - - - - Monitoring - - - - - - -Allow log_min_messages log levels to be specified by process type (Euler Taveira) -§ - - - -The new format is type:level. A value without a colon controls all process types, allowing backward compatibility. - - - - - - - -Add server variable log_autoanalyze_min_duration to log long-running analyze operations by autovacuum operations (Shinya Kato) -§ - - - -Server variable log_autovacuum_min_duration now only controls logging of vacuum operations by autovacuum. - - - - - - - -Enable server variable log_lock_waits by default (Laurenz Albe) -§ - - - - - - - -Add server variable debug_print_raw_parse to log raw parse trees (Chao Li) -§ - - - -This is also enabled when the server is started with debug level three and higher. - - - - - - - -Make messages coming from remote servers appear in the server logs in the same format as local server messages (Vignesh C) -§ - - - -These include replication, , and servers. - - - - - - - -Add reporting of WAL full-page write bytes to VACUUM and ANALYZE logging (Shinya Kato) -§ - - - - - - - -Add IO wait events for COPY FROM/TO on a pipe, file, or program (Nikolay Samokhvalov) -§ - - - - - - - -Add wait events for WAL write and flush LSNs (Xuneng Zhou) -§ - - - - - - - -Have pg_get_sequence_data() return the sequence page LSN (Vignesh C) -§ - - - - - - - -Add function pg_get_multixact_stats() to report multixact activity (Naga Appani) -§ - - - - - - - -Issue warnings when the wraparound of xid and multi-xids is less than 100 million (Nathan Bossart) -§ - - - -The previous warning was 40 million. Warnings are issued to clients and in the server log. - - - - - - - - - Server Configuration - - - - - - -Allow online enabling and disabling of data checksums (Daniel Gustafsson, Magnus Hagander, Tomas Vondra) -§ -§ - - - -Previously the checksum status could only be changed while the cluster was offline using pg_checksums. - - - - - - - -Add scoring system to control the order that tables are processed by autovacuum (Nathan Bossart) -§ - - - -The new server variables are autovacuum_freeze_score_weight, autovacuum_multixact_freeze_score_weight, autovacuum_vacuum_score_weight, vacuum_insert_score_weight, and -autovacuum_analyze_score_weight. - - - - - - - -Add server-side support for SNI (Server Name Indication) (Daniel Gustafsson, Jacob Champion) -§ - - - -New configuration file PGDATA/pg_hosts.conf specifies hostname/key pairs. - - - - - - - -Add a new OAUTH flow hook PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 (Jacob Champion) -§ -§ - - - -This is an improved version of PQAUTHDATA_OAUTH_BEARER_TOKEN by adding the issuer identifier and error message specification. - - - - - - - -Allow roles pg_read_all_data and pg_write_all_data to read/write large objects (Nitin Motiani, Nathan Bossart) -§ - - - -These roles are designed to allow non-super users to run pg_dump. - - - - - - - -Allow background workers to be configured to terminate before database-level operations (Aya Iwata) -§ - - - -This allows database-level operations to complete more quickly since blocking background workers can now be terminated. - - - - - - - -Allow server variables that represent lists to be emptied by setting the value to NULL (Tom Lane) -§ - - - - - - - -Update GB18030 encoding from version 2000 to 2022 (Chao Li, Zheng Tao) -§ - - - -See the commit message for compatibility details. - - - - - - - - - <link linkend="streaming-replication">Streaming Replication and Recovery</link> - - - - - - -Add WAIT FOR command to allow standbys to wait for LSN values to be written, flushed, or replayed (Kartyshov Ivan, Alexander Korotkov, Xuneng Zhou) -§ -§ - - - - - - - -Improve function pg_sync_replication_slots() to wait for replication synchronization completion (Ajin Cherian, Zhijie Hou) -§ - - - -Previously, certain synchronization failures would not be reported. - - - - - - - -Add server variable wal_sender_shutdown_timeout to limit replica synchronization waits during shutdown (Andrey Silitskiy, Hayato Kuroda) -§ - - - -By default, senders still wait forever for synchronization. - - - - - - - -Allow wal_receiver_timeout to be set per-subscription and user (Fujii Masao) -§ -§ - - - -This allows subscribers to use different wal_receiver_timeout values. - - - - - - - -Add optional pid parameter to pg_replication_origin_session_setup() to allow parallelization of SQL-level replication solutions (Doruk Yilmaz, Hayato Kuroda) -§ - - - - - - - - - <link linkend="logical-replication">Logical Replication</link> - - - - - - -Allow sequence values stored in subscribers to match the publisher (Vignesh C) -§ -§ -§ - - - -This is enabled during CREATE SUBSCRIPTION, ALTER SUBSCRIPTION ... REFRESH PUBLICATION, and ALTER SUBSCRIPTION ... REFRESH SEQUENCES. The latter only updates values, not sequence -existence. Function pg_get_sequence_data() allows inspection of sequence synchronization. - - - - - - - -Allow CREATE/ALTER PUBLICATION to publish all sequences (Vignesh C, Tomas Vondra) -§ - - - -This is enabled with the ALL SEQUENCES clause. - - - - - - - -Allow ALTER SUBSCRIPTION on publications to synchronize the existence of sequences on subscribers to match the publisher (Vignesh C) -§ - - - -This is enabled with the REFRESH SEQUENCES clause. - - - - - - - -Allow CREATE/ALTER PUBLICATION to exclude some tables (Vignesh C, Shlok Kyal) -§ -§ -§ -§ - - - -This is controlled with the EXCEPT clause, and is useful when specifying ALL TABLES. - - - - - - - -Add CREATE/ALTER PUBLICATION setting retain_dead_tuples to retain information needed for conflict resolution (Zhijie Hou) -§ -§ -§ - - - -Also add setting max_retention_duration to limit retain_dead_tuples retention. - - - - - - - -Allow CREATE SUBSCRIPTION to use foreign data wrapper connection parameters (Jeff Davis) -§ - - - -The connection parameters are referenced via CREATE SUBSCRIPTION ... SERVER. - - - - - - - -When server variable wal_level is replica, allow automatic enablement of logical replication when needed (Masahiko Sawada) -§ - - - -New server variable effective_wal_level reports the effective WAL level. - - - - - - - - - - - Query Commands - - - - - - -Add support for SQL Property Graph Queries (SQL/PGQ) (Peter Eisentraut, Ashutosh Bapat) -§ -§ -§ - - - -Internally these are processed like views so are written as standard relational queries. - - - - - - - -Add FOR PORTION OF clause to UPDATE and DELETE (Paul A. Jungwirth) -§ -§ - - - -This allows operations on temporal ranges. - - - - - - - -Add GROUP BY ALL syntax to SELECT to automatically group all non-aggregate and non-window-function target list parameters (David Christensen) -§ - - - - - - - -Allow GROUP BY to process target list subqueries that have expressions referencing non-subquery columns (Tom Lane) -§ - - - -Also fix a bug in how GROUPING() handles target list subquery aliases. - - - - - - - -Allow window functions to ignore NULLs with the IGNORE NULLS/RESPECT NULLS clause (Oliver Ford, Tatsuo Ishii) -§ - - - -Supported window functions are lead(), lag(), first_value(), last_value(), and nth_value(). - - - - - - - -Add support for INSERT ... ON CONFLICT DO SELECT ... RETURNING (Andreas Karlsson, Marko Tiikkaja, Viktor Holmberg) -§ - - - -This allows conflicting rows to be returned, and optionally locked with FOR UPDATE/SHARE. - - - - - - - - - Utility Commands - - - - - - -Add REPACK command which replaces VACUUM FULL and CLUSTER (Antonin Houska) -§ - - - -The two former commands did similar things, but with confusing names, so unify them as REPACK. The old commands have been retained for compatibility. - - - - - - - -Allow REPACK to rebuild tables without access-exclusive locking (Antonin Houska, Mihail Nikalayeu, Álvaro Herrera) -§ -§ -§ - - - -This is enabled via the CONCURRENTLY option. Server variable max_repack_replication_slots was also added. - - - - - - - -Allow partitions to be merged and split using ALTER TABLE ... MERGE/SPLIT PARTITIONS (Dmitry Koval, Alexander Korotkov, Tender Wang, Richard Guo, Dagfinn Ilmari Mannsåker, Fujii Masao, Jian He) -§ -§ - - - - - - - -Allow GRANT/REVOKE to specify the effective role performing the privileges adjustment (Nathan Bossart, Tom Lane) -§ - - - -The GRANTED BY clause controls this. - - - - - - - -Allow CREATE SCHEMA to create more types of objects in newly-created schemas (Kirill Reshke, Jian He, Tom Lane) -§ - - - - - - - -Allow CHECKPOINT to accept a list of options (Christoph Berg) -§ -§ -§ - - - -Supported options are MODE and FLUSH_UNLOGGED. - - - - - - - -Add CONNECTION clause to CREATE FOREIGN DATA WRAPPER to specify a function to be called for subscription connection parameters (Jeff Davis, Noriyoshi Shinoda) -§ -§ - - - - - - - -Add memory usage and parallelism reporting to VACUUM (VERBOSE) and autovacuum logs (Tatsuya Kawata, Daniil Davydov) -§ -§ - - - - - - - <link linkend="ddl-constraints">Constraints</link> - - - - - - -Allow ALTER TABLE ALTER CONSTRAINT ... [NOT] ENFORCED for CHECK constraints (Jian He) -§ - - - -Previously enforcement changes were only supported for foreign key constraints. - - - - - - - -Allow ALTER TABLE ... COLUMN SET EXPRESSION to succeed on virtual columns with CHECK constraints (Jian He) -§ - - - -This was previously prohibited. - - - - - - - - <xref linkend="sql-copy"/> - - - - - - -Allow multiple headers lines to be skipped by COPY FROM (Shinya Kato, Fujii Masao) -§ - - - -Previously only a single header line could be skipped. - - - - - - - -Allow COPY FROM to set invalid input values to NULL (Jian He, Kirill Reshke) -§ - - - -This is done using the COPY option ON_ERROR SET_NULL. - - - - - - - -Allow COPY TO to output JSON format (Joe Conway, Jian He, Andrew Dunstan) -§ -§ - - - -JSON output can also be a single JSON array using the COPY option FORCE_ARRAY. - - - - - - - -Allow COPY TO to process partitioned tables (Jian He, Ajin Cherian) -§ -§ - - - -Previously COPY (SELECT ...) had to be used to output partitioned tables. This also improves logical replication table synchronization. - - - - - - - - <xref linkend="sql-explain"/> - - - - - - -Add EXPLAIN ANALYZE option IO to report asynchronous IO activity (Tomas Vondra) -§ -§ -§ - - - - - - - -Add reporting of WAL full-page write bytes to EXPLAIN (ANALYZE, WAL) output (Shinya Kato) -§ - - - - - - - -Add Memoize cache and lookup estimates to EXPLAIN output (Ilia Evdokimov, Lukas Fittl) -§ - - - -This can show why Memoize was chosen. - - - - - - - - - - - Data Types - - - - - - -Add the 64-bit unsigned data type oid8 (Michael Paquier) -§ - - - - - - - -Add more jsonpath string methods (Florents Tselai, David E. Wheeler) -§ - - - -They are ltrim(), rtrim(), btrim(), lower(), upper(), initcap(), replace(), and split_part(). These are immutable like their non-JSON string variants. - - - - - - - -Allow casts between bytea and uuid data types (Dagfinn Ilmari Mannsåker, Aleksander Alekseev) -§ - - - - - - - -Add ability to cast between database names and oid8s using regdatabase (Ian Lawrence Barwick) -§ - - - - - - - -Add functions tid_block() and tid_offset() to extract block numbers and offsets from tid values (Ayush Tiwari) -§ - - - - - - - - - Functions - - - - - - -Add date, timestamp, and timestamptz versions of random(min, max) (Damien Clochard, Dean Rasheed) -§ -§ - - - - - - - -Allow encode() and decode() to process data in base64url and base32hex formats (Andrey Borodin, Aleksander Alekseev, Florents Tselai) -§ -§ -§ - - - -This format retains ordering, unlike base32. - - - - - - - -Add functions to return a set of ranges resulting from range subtraction (Paul A. Jungwirth) -§ - - - -The functions are range_minus_multi() and multirange_minus_multi(). This is useful to represent range subtraction results with gaps. - - - - - - - -Add function error_on_null() to return the supplied parameter, or error on NULL input (Joel Jacobson) -§ - - - - - - - -Allow IS JSON to work on domains defined over supported base types (Jian He) -§ - - - -The supported base types are TEXT, JSON, JSONB, and BYTEA. - - - - - - - -Add full text stemmers for Polish and Esperanto (Tom Lane) -§ - - - -The Dutch stemmer has also been updated. The old Dutch stemmer is available via dutch_porter. - - - - - - - -Add function pg_get_role_ddl() to output role creation commands (Mario Gonzalez, Bryan Green, Andrew Dunstan, Euler Taveira) -§ - - - - - - - -Add function pg_get_tablespace_ddl() to output tablespace creation commands (Nishant Sharma, Manni Wood, Andrew Dunstan, Euler Taveira) -§ - - - - - - - -Add function pg_get_database_ddl() to output database creation commands (Akshay Joshi, Andrew Dunstan, Euler Taveira) -§ - - - - - - - -Allow event triggers to be written using PL/Python (Euler Taveira, Dimitri Fontaine) -§ - - - - - - - - - <link linkend="libpq">Libpq</link> - - - - - - -Allow libpq connections to specify a service file via servicefile (Torsten Förtsch, Ryo Kanbayashi) -§ - - - - - - - -Add special libpq protocol version 3.9999 for version testing (Jelte Fennema-Nio) -§ - - - - - - - -Add libpq function PQgetThreadLock() to retrieve the current locking callback (Jacob Champion) -§ - - - - - - - -Add libpq connection parameter oauth_ca_file to specify the OAUTH certificate authority file (Jonathan Gonzalez V., Jacob Champion) -§ - - - -This can also be set via the PGOAUTHCAFILE environment variable. The default is to use curl's built-in certificates. - - - - - - - -Allow custom OAUTH validators to register custom pg_hba.conf authentication options (Jacob Champion) -§ - - - - - - - -Allow OAUTH validators to supply failure details (Jacob Champion) -§ - - - -This is done by setting the ValidatorModuleResult structure member error_detail. - - - - - - - -Allow libpq environment variable PGOAUTHDEBUG to specify particular debug options (Zsolt Parragi, Jacob Champion) -§ - - - -The UNSAFE option still generates all debugging output. - - - - - - - - - <xref linkend="app-psql"/> - - - - - - -Allow the search path to appear in the psql prompt via %S (Florents Tselai) -§ - - - -This works when psql is connected to PostgreSQL 18 or later. - - - - - - - -Allow the hot standby status to appear in the psql prompt via %i (Jim Jones) -§ - - - - - - - -Modify psql backslash commands to show comments for publications, subscriptions, and extended statistics (Fujii Masao, Jim Jones) -§ - - - -The modified commands are \dRp+, \dRs+, and \dX+. - - - - - - - -Allow control over how booleans are displayed in psql (David G. Johnston) -§ - - - -The \pset variables are display_true and display_false. - - - - - - - -Add psql variable SERVICEFILE to reference the service file location (Ryo Kanbayashi) -§ - - - - - - - -Allow psql to more accurately determine if the pager is needed (Erik Wienhold) -§ - - - - - - - -Add or improve psql tab completion (Yamaguchi Atsuo, Yugo Nagata, Haruna Miwa, Xuneng Zhou, Dagfinn Ilmari Mannsåker, Fujii Masao, Álvaro Herrera, Jian He, Tatsuya Kawata, Ian Lawrence Barwick, Vasuki M) -§ -§ -§ -§ -§ -§ -§ -§ -§ -§ -§ -§ -§ -§ - - - - - - - - - Server Applications - - - - - - -Change vacuumdb's and options to analyze partitioned tables when no targets are specified (Laurenz Albe, Mircea Cadariu, Chao Li) -§ -§ - - - -Previously it skipped partitioned tables. This now matches the behavior of ANALYZE. - - - - - - - -Allow vacuumdb to report its commands without running them using option (Corey Huinker) -§ - - - - - - - -Allow pg_verifybackup to read WAL files stored in tar archives (Amul Sul) -§ - - - -Add option as an alias for the existing and deprecated option. - - - - - - - -Allow pg_waldump to read WAL files stored in tar archives (Amul Sul) -§ - - - - - - - -Improve performance of pg_upgrade copying large object metadata (Nathan Bossart) -§ -§ -§ -§ - - - -Various methods are used, depending on the PostgreSQL version of the old cluster. - - - - - - - -Allow pg_upgrade to process non-default tablespaces stored in the PGDATA directory (Nathan Bossart) -§ - - - -Previously such tablespaces generated an error. - - - - - - - -Add pgbench option to continue after SQL errors (Rintaro Ikeda, Yugo Nagata, Fujii Masao) -§ - - - - - - - -Improve the usability of pg_test_timing (Hannu Krosing, Tom Lane) -§ -§ - - - -Report nanoseconds instead of microseconds. In addition to histogram output, output a second table that reports exact timings, with an optional cutoff set by . - - - - - - - <link - linkend="app-pgdump"><application>pg_dump</application></link>/<link - linkend="app-pg-dumpall"><application>pg_dumpall</application></link>/<link - linkend="app-pgrestore"><application>pg_restore</application></link> - - - - - - -Allow pg_dump to include restorable extended statistics (Corey Huinker) -§ - - - - - - - - - <link linkend="app-pgcreatesubscriber"><application>pg_createsubscriber</application></link> - - - - - - -Allow pg_createsubscriber to ignore specified publications that already exist (Shubham Khanna) -§ - - - -Previously this generated an error. - - - - - - - -Change the way pg_createsubscriber stores recovery parameters (Alyona Vinter) -§ - - - -Changes are stored in optionally-included pg_createsubscriber.conf rather than directly in postgresql.auto.conf. - - - - - - - -Add pg_createsubscriber option / to redirect output to files (Gyan Sreejith, Hayato Kuroda) -§ - - - - - - - - - - - Source Code - - - - - - -Restore support for AIX (Aditya Kamath, Srirama Kucherlapati, Peter Eisentraut) -§ -§ - - - -This uses gcc and only supports 64-bit builds. - - - - - - - -Change Solaris to use unnamed POSIX semaphores (Tom Lane) -§ - - - -Previously it used System V semaphores. - - - - - - - -Require Visual Studio 2019 or later (Peter Eisentraut) -§ - - - - - - - -Allow MSVC to create PL/Python using the Python Limited API (Bryan Green) -§ - - - - - - - -Allow building on AArch64 using MSVC (Niyas Sait, Greg Burd, Dave Cramer) -§ - - - - - - - -Allow execution stack backtraces on Windows using DbgHelp (Bryan Green) -§ - - - - - - - -Change the supported C language version to C11 (Peter Eisentraut) -§ -§ - - - -Previously C99 was used. - - - - - - - -Use standard C23 and C++ attributes if available (Peter Eisentraut) -§ - - - - - - - -Use AVX2 CPU instructions for calculating page checksums (Matthew Sterrett, Andrew Kim) -§ - - - - - - - -Use ARM Crypto Extension to Compute CRC32C (John Naylor) -§ - - - - - - - -Change hex_encode() and hex_decode() to use SIMD CPU instructions (Nathan Bossart, Chiranmoy Bhattacharya) -§ - - - - - - - -Require Meson version 0.57.2 or later (Peter Eisentraut) -§ - - - - - - - -Add Meson option to build both shared and static libraries, or only shared (Peter Eisentraut) -§ - - - - - - - -Update Unicode data to version 17.0.0 (Peter Eisentraut) -§ - - - - - - - -Add hooks planner_setup_hook, planner_shutdown_hook, joinrel_setup_hook, and join_path_setup_hook (Robert Haas) -§ -§ - - - - - - - -Allow extensions to replace set-returning functions in the FROM clause with SQL queries (Paul A. Jungwirth) -§ - - - - - - - -Make multixid members 64-bit (Maxim Orlov) -§ - - - - - - - -Change function prototypes to use uint* instead of bit* typedefs (Nathan Bossart) -§ - - - - - - - -Allow logical decoding plugins to specify if they do not access shared catalogs (Antonin Houska) -§ - - - - - - - -Add simplified and improved shared memory registration function ShmemRequestStruct() (Heikki Linnakangas, Ashutosh Bapat) -§ - - - -Functions ShmemInitStruct() and ShmemInitHash() remain for backward compatibility. - - - - - - - -Add server variable debug_exec_backend to report how parameters are passed to new backends (Daniel Gustafsson) -§ - - - - - - - -Add documentation section about temporal tables (Paul A. Jungwirth) -§ - - - - - - - -Document the environment variables that control the regression tests (Michael Paquier) -§ - - - - - - - -Update documented systemd example to include a restart setting (Andrew Jackson) -§ - - - - - - - - - Additional Modules - - - - - - -Add module to stabilize and control planner decisions (Robert Haas) -§ -§ - - - - - - - -Add extension to allow per-query-id advice to be specified (Robert Haas, Lukas Fittl) -§ -§ - - - - - - - -Show sizes of FETCH queries as constants in (Sami Imseih) -§ - - - -Fetches of different sizes will now be grouped together in output. - - - - - - - -Add generic and custom plan counts to (Sami Imseih) -§ - - - - - - - -Refactor reporting of shared memory mapping (Bertrand Drouvot) -§ - - - -New function pg_buffercache_os_pages() and system view pg_buffercache_os_pages allow reporting of shared memory mapping; the function optionally includes NUMA details. Function -pg_buffercache_numa_pages() remains for backward compatibility. - - - - - - - -Add functions to to mark buffers as dirty (Nazir Bilal Yavuz) -§ - - - -The functions are pg_buffercache_mark_dirty(), pg_buffercache_mark_dirty_relation(), and pg_buffercache_mark_dirty_all(). - - - - - - - -Allow pushdown of array comparisons in prepared statements to foreign servers (Alexander Pyhalov) -§ - - - - - - - -Allow the retrieval of statistics from foreign data wrapper servers (Corey Huinker, Etsuro Fujita) -§ - - - -This is enabled for by using the option restore_stats. The default is for ANALYZE to retrieve rows from the remote server to locally generate statistics. - - - - - - - -Allow to read files or program output that uses multi-line headers (Shinya Kato) -§ - - - - - - - -Add server variable auto_explain.log_io to add IO reporting to auto_explain (Tomas Vondra) -§ - - - - - - - -Allow auto_explain to add extension-specific EXPLAIN options via server variable auto_explain.log_extension_options (Robert Haas) -§ - - - - - - - -Change to support all btree-supported cross-type comparisons (Tom Lane) -§ -§ - - - - - - - -Improve performance of indexes by using streaming reads (Xuneng Zhou) -§ -§ - - - - - - - -Improve performance of by using streaming reads (Xuneng Zhou) -§ -§ - - - - - - - -Allow 's dmetaphone() to use single-byte encodings beyond ASCII (Peter Eisentraut) -§ - - - - - - - -Modify oid2name to report the relation file path (David Bidoc) -§ - - - - - - - - - - - Acknowledgments - - - The following individuals (in alphabetical order) have contributed - to this release as patch authors, committers, reviewers, testers, - or reporters of issues. - - - - fill in later - - - - diff --git a/doc/src/sgml/release-20.sgml b/doc/src/sgml/release-20.sgml new file mode 100644 index 0000000000000..5fe36c1d7d263 --- /dev/null +++ b/doc/src/sgml/release-20.sgml @@ -0,0 +1,16 @@ + + + + + Release 20 + + + Release date: + 2026-??-?? + + + + This is just a placeholder for now. + + + diff --git a/doc/src/sgml/release.sgml b/doc/src/sgml/release.sgml index ae6cc3cc2cb57..9d861a0f8c0e9 100644 --- a/doc/src/sgml/release.sgml +++ b/doc/src/sgml/release.sgml @@ -70,7 +70,7 @@ For new features, add links to the documentation sections. All the active branches have to be edited concurrently when doing that. --> -&release-19; +&release-20; Prior Releases diff --git a/meson.build b/meson.build index 568e0e150bfa8..d88a7a7030878 100644 --- a/meson.build +++ b/meson.build @@ -8,7 +8,7 @@ project('postgresql', ['c'], - version: '19beta1', + version: '20devel', license: 'PostgreSQL', # We want < 0.62 for python 3.6 compatibility on old platforms. diff --git a/src/tools/git_changelog b/src/tools/git_changelog index d5f651ecf5b0e..c92c6cfe3e32d 100755 --- a/src/tools/git_changelog +++ b/src/tools/git_changelog @@ -59,7 +59,7 @@ require IPC::Open2; # (We could get this from "git branches", but not worth the trouble.) # NB: master must be first! my @BRANCHES = qw(master - REL_18_STABLE + REL_19_STABLE REL_18_STABLE REL_17_STABLE REL_16_STABLE REL_15_STABLE REL_14_STABLE REL_13_STABLE REL_12_STABLE REL_11_STABLE REL_10_STABLE REL9_6_STABLE REL9_5_STABLE REL9_4_STABLE REL9_3_STABLE REL9_2_STABLE REL9_1_STABLE REL9_0_STABLE diff --git a/src/tools/version_stamp.pl b/src/tools/version_stamp.pl index ea50ee41c691e..8f94e9e288697 100755 --- a/src/tools/version_stamp.pl +++ b/src/tools/version_stamp.pl @@ -25,7 +25,7 @@ # Major version is hard-wired into the script. We update it when we branch # a new development version. -my $majorversion = 19; +my $majorversion = 20; # Validate argument and compute derived variables my $minor = shift; From efa59a500457f310abbc38dc472f03e959ccd5b8 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 30 Jun 2026 08:30:08 +0900 Subject: [PATCH 21/36] Simplify some stats restore code with InputFunctionCallSafe() statatt_build_stavalues() and array_in_safe() have been relying on InitFunctionCallInfoData() with a locally-filled state to call a data type input function. InputFunctionCallSafe() can be used to achieve the same job, simplifying some code. This fixes an over-allocation of FunctionCallInfoBaseData done in statatt_build_stavalues(), where there was space for 8 elements but only 3 were needed. The over-allocation exists since REL_18_STABLE, and was harmless in practice. While on it, fix some comments for both routines, where elemtypid was mentioned. Backpatch down to v19. This code has been reworked during the last development cycle while working on the restore of extended statistics, so this keeps the code consistent across all branches. Author: Jian He Author: Michael Paquier Discussion: https://postgr.es/m/CACJufxEGah9PaiTQ=cG14GMMBsUQ3ohGct9tdSwbMQPQ0-nbbQ@mail.gmail.com Backpatch-through: 19 --- src/backend/statistics/extended_stats_funcs.c | 17 +++----------- src/backend/statistics/stat_utils.c | 23 +++++-------------- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c index 2cb3942056f9a..a5dce8a220641 100644 --- a/src/backend/statistics/extended_stats_funcs.c +++ b/src/backend/statistics/extended_stats_funcs.c @@ -1033,7 +1033,7 @@ jbv_to_infunc_datum(JsonbValue *jval, PGFunction func, AttrNumber exprnum, } /* - * Build an array datum with element type elemtypid from a text datum, used as + * Build an array datum with element type typid from a text datum, used as * value of an attribute in a pg_statistic tuple. * * If an error is encountered, capture it, and reduce the elevel to WARNING. @@ -1044,7 +1044,6 @@ static Datum array_in_safe(FmgrInfo *array_in, const char *s, Oid typid, int32 typmod, AttrNumber exprnum, const char *element_name, bool *ok) { - LOCAL_FCINFO(fcinfo, 3); Datum result; ErrorSaveContext escontext = { @@ -1053,17 +1052,6 @@ array_in_safe(FmgrInfo *array_in, const char *s, Oid typid, int32 typmod, }; *ok = false; - InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid, - (Node *) &escontext, NULL); - - fcinfo->args[0].value = CStringGetDatum(s); - fcinfo->args[0].isnull = false; - fcinfo->args[1].value = ObjectIdGetDatum(typid); - fcinfo->args[1].isnull = false; - fcinfo->args[2].value = Int32GetDatum(typmod); - fcinfo->args[2].isnull = false; - - result = FunctionCallInvoke(fcinfo); /* * If the array_in function returned an error, we will want to report that @@ -1071,7 +1059,8 @@ array_in_safe(FmgrInfo *array_in, const char *s, Oid typid, int32 typmod, * Overwriting the existing hint (if any) is not ideal, and an error * context would only work for level >= ERROR. */ - if (escontext.error_occurred) + if (!InputFunctionCallSafe(array_in, (char *) s, typid, typmod, + (Node *) &escontext, &result)) { StringInfoData hint_str; diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c index a673e3c704b25..0b190e8823703 100644 --- a/src/backend/statistics/stat_utils.c +++ b/src/backend/statistics/stat_utils.c @@ -555,7 +555,7 @@ statatt_get_elem_type(Oid atttypid, char atttyptype, } /* - * Build an array with element type elemtypid from a text datum, used as + * Build an array with element type typid from a text datum, used as * value of an attribute in a tuple to-be-inserted into pg_statistic. * * The typid and typmod should be derived from a previous call to @@ -569,7 +569,6 @@ Datum statatt_build_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid, int32 typmod, bool *ok) { - LOCAL_FCINFO(fcinfo, 8); char *s; Datum result; ErrorSaveContext escontext = {T_ErrorSaveContext}; @@ -578,28 +577,18 @@ statatt_build_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid ty s = TextDatumGetCString(d); - InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid, - (Node *) &escontext, NULL); - - fcinfo->args[0].value = CStringGetDatum(s); - fcinfo->args[0].isnull = false; - fcinfo->args[1].value = ObjectIdGetDatum(typid); - fcinfo->args[1].isnull = false; - fcinfo->args[2].value = Int32GetDatum(typmod); - fcinfo->args[2].isnull = false; - - result = FunctionCallInvoke(fcinfo); - - pfree(s); - - if (escontext.error_occurred) + if (!InputFunctionCallSafe(array_in, s, typid, typmod, + (Node *) &escontext, &result)) { + pfree(s); escontext.error_data->elevel = WARNING; ThrowErrorData(escontext.error_data); *ok = false; return (Datum) 0; } + pfree(s); + if (ARR_NDIM(DatumGetArrayTypeP(result)) != 1) { ereport(WARNING, From 8e684ce11ddaac2604375b8efcae97f6b36af4e7 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 30 Jun 2026 08:48:47 +0900 Subject: [PATCH 22/36] Fix unlogged sequence corruption after standby promotion Previously, if an unlogged sequence was created on the primary and replicated to a standby, reading the sequence after promoting the standby (for example, with nextval()) could trigger the following assertion failure: TRAP: failed Assert("((const PageHeaderData *) page)->pd_special >= SizeOfPageHeaderData") In non-assert builds, the same operation could instead fail with an error such as: ERROR: bad magic number in sequence The problem was that seq_redo() updated the init fork page in shared buffers but did not flush it to disk. During promotion, ResetUnloggedRelations() recreates the main fork of unlogged relations by copying the init fork from disk, bypassing shared buffers. As a result, the main fork could be recreated from a stale init fork instead of the WAL-replayed page. Fix this by introducing a helper to flush init fork buffers immediately, and make seq_redo() use it. As a result, the main fork of an unlogged sequence is recreated from the up-to-date init fork on disk, allowing the unlogged sequence to be read successfully after standby promotion. Backpatch to v15, where unlogged sequences were introduced. Author: Fujii Masao Reviewed-by: vignesh C Discussion: https://postgr.es/m/CAHGQGwH1Ssze3XM6wjoTjSLVOR041c6xP+vsdLP951=w8oG8bA@mail.gmail.com Backpatch-through: 15 --- src/backend/access/hash/hash_xlog.c | 29 ++-------------- src/backend/access/transam/xlogutils.c | 26 +++++++++++++- src/backend/commands/sequence_xlog.c | 1 + src/include/access/xlogutils.h | 2 ++ src/test/recovery/meson.build | 1 + .../t/054_unlogged_sequence_promotion.pl | 34 +++++++++++++++++++ 6 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 src/test/recovery/t/054_unlogged_sequence_promotion.pl diff --git a/src/backend/access/hash/hash_xlog.c b/src/backend/access/hash/hash_xlog.c index 2060620c7dec9..e9a2b9aa9a723 100644 --- a/src/backend/access/hash/hash_xlog.c +++ b/src/backend/access/hash/hash_xlog.c @@ -29,7 +29,6 @@ hash_xlog_init_meta_page(XLogReaderState *record) XLogRecPtr lsn = record->EndRecPtr; Page page; Buffer metabuf; - ForkNumber forknum; xl_hash_init_meta_page *xlrec = (xl_hash_init_meta_page *) XLogRecGetData(record); @@ -41,16 +40,7 @@ hash_xlog_init_meta_page(XLogReaderState *record) page = BufferGetPage(metabuf); PageSetLSN(page, lsn); MarkBufferDirty(metabuf); - - /* - * Force the on-disk state of init forks to always be in sync with the - * state in shared buffers. See XLogReadBufferForRedoExtended. We need - * special handling for init forks as create index operations don't log a - * full page image of the metapage. - */ - XLogRecGetBlockTag(record, 0, NULL, &forknum, NULL); - if (forknum == INIT_FORKNUM) - FlushOneBuffer(metabuf); + XLogFlushBufferForRedoIfInit(record, 0, metabuf); /* all done */ UnlockReleaseBuffer(metabuf); @@ -68,7 +58,6 @@ hash_xlog_init_bitmap_page(XLogReaderState *record) Page page; HashMetaPage metap; uint32 num_buckets; - ForkNumber forknum; xl_hash_init_bitmap_page *xlrec = (xl_hash_init_bitmap_page *) XLogRecGetData(record); @@ -79,16 +68,7 @@ hash_xlog_init_bitmap_page(XLogReaderState *record) _hash_initbitmapbuffer(bitmapbuf, xlrec->bmsize, true); PageSetLSN(BufferGetPage(bitmapbuf), lsn); MarkBufferDirty(bitmapbuf); - - /* - * Force the on-disk state of init forks to always be in sync with the - * state in shared buffers. See XLogReadBufferForRedoExtended. We need - * special handling for init forks as create index operations don't log a - * full page image of the metapage. - */ - XLogRecGetBlockTag(record, 0, NULL, &forknum, NULL); - if (forknum == INIT_FORKNUM) - FlushOneBuffer(bitmapbuf); + XLogFlushBufferForRedoIfInit(record, 0, bitmapbuf); UnlockReleaseBuffer(bitmapbuf); /* add the new bitmap page to the metapage's list of bitmaps */ @@ -109,10 +89,7 @@ hash_xlog_init_bitmap_page(XLogReaderState *record) PageSetLSN(page, lsn); MarkBufferDirty(metabuf); - - XLogRecGetBlockTag(record, 1, NULL, &forknum, NULL); - if (forknum == INIT_FORKNUM) - FlushOneBuffer(metabuf); + XLogFlushBufferForRedoIfInit(record, 1, metabuf); } if (BufferIsValid(metabuf)) UnlockReleaseBuffer(metabuf); diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c index fdc341d8fa48b..d8c179c5dccb2 100644 --- a/src/backend/access/transam/xlogutils.c +++ b/src/backend/access/transam/xlogutils.c @@ -321,6 +321,28 @@ XLogInitBufferForRedo(XLogReaderState *record, uint8 block_id) return buf; } +/* + * If a redo routine modified an init fork, flush the buffer immediately. + * + * At the end of crash recovery the init forks of unlogged relations are + * copied to the main fork directly from disk, without going through shared + * buffers. Therefore, redo routines that update init forks without + * restoring a full-page image must call this after setting the page LSN and + * marking the buffer dirty. + */ +void +XLogFlushBufferForRedoIfInit(XLogReaderState *record, uint8 block_id, + Buffer buffer) +{ + ForkNumber forknum; + + Assert(BufferIsValid(buffer)); + + XLogRecGetBlockTag(record, block_id, NULL, &forknum, NULL); + if (forknum == INIT_FORKNUM) + FlushOneBuffer(buffer); +} + /* * XLogReadBufferForRedoExtended * Like XLogReadBufferForRedo, but with extra options. @@ -398,7 +420,9 @@ XLogReadBufferForRedoExtended(XLogReaderState *record, * At the end of crash recovery the init forks of unlogged relations * are copied, without going through shared buffers. So we need to * force the on-disk state of init forks to always be in sync with the - * state in shared buffers. + * state in shared buffers. Use XLogFlushBufferForRedoIfInit() for + * redo routines that dirty init-fork buffers without restoring a + * full-page image. */ if (forknum == INIT_FORKNUM) FlushOneBuffer(*buf); diff --git a/src/backend/commands/sequence_xlog.c b/src/backend/commands/sequence_xlog.c index d0aed48e26801..fcb3230cf3be0 100644 --- a/src/backend/commands/sequence_xlog.c +++ b/src/backend/commands/sequence_xlog.c @@ -63,6 +63,7 @@ seq_redo(XLogReaderState *record) memcpy(page, localpage, BufferGetPageSize(buffer)); MarkBufferDirty(buffer); + XLogFlushBufferForRedoIfInit(record, 0, buffer); UnlockReleaseBuffer(buffer); pfree(localpage); diff --git a/src/include/access/xlogutils.h b/src/include/access/xlogutils.h index b97387c6d4c43..0c6c74100694f 100644 --- a/src/include/access/xlogutils.h +++ b/src/include/access/xlogutils.h @@ -87,6 +87,8 @@ typedef struct ReadLocalXLogPageNoWaitPrivate extern XLogRedoAction XLogReadBufferForRedo(XLogReaderState *record, uint8 block_id, Buffer *buf); extern Buffer XLogInitBufferForRedo(XLogReaderState *record, uint8 block_id); +extern void XLogFlushBufferForRedoIfInit(XLogReaderState *record, + uint8 block_id, Buffer buffer); extern XLogRedoAction XLogReadBufferForRedoExtended(XLogReaderState *record, uint8 block_id, ReadBufferMode mode, bool get_cleanup_lock, diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build index 9eb8ed114254a..ad0d85f41897e 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -62,6 +62,7 @@ tests += { 't/051_effective_wal_level.pl', 't/052_checkpoint_segment_missing.pl', 't/053_standby_login_event_trigger.pl', + 't/054_unlogged_sequence_promotion.pl', ], }, } diff --git a/src/test/recovery/t/054_unlogged_sequence_promotion.pl b/src/test/recovery/t/054_unlogged_sequence_promotion.pl new file mode 100644 index 0000000000000..96d1e4bf18be4 --- /dev/null +++ b/src/test/recovery/t/054_unlogged_sequence_promotion.pl @@ -0,0 +1,34 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test that unlogged sequences created on a primary can be read after +# promotion of a standby that replayed their init fork. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node_primary = PostgreSQL::Test::Cluster->new('primary'); +$node_primary->init(allows_streaming => 1); +$node_primary->start; + +my $backup_name = 'my_backup'; +$node_primary->backup($backup_name); + +my $node_standby = PostgreSQL::Test::Cluster->new('standby'); +$node_standby->init_from_backup($node_primary, $backup_name, + has_streaming => 1); +$node_standby->start; + +# Create the unlogged sequence after the standby has started, so its init fork +# is generated by WAL replay on the standby. +$node_primary->safe_psql('postgres', "CREATE UNLOGGED SEQUENCE ulseq"); +$node_primary->wait_for_replay_catchup($node_standby); + +$node_standby->promote; + +is($node_standby->safe_psql('postgres', "SELECT nextval('ulseq')"), + 1, 'unlogged sequence can be read after standby promotion'); + +done_testing(); From d8113095c488ad588a0890833f67d01df46374e7 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 30 Jun 2026 10:28:52 +0900 Subject: [PATCH 23/36] Remove stray blank line in ParseFuncOrColumn() Commit 419ce13b701 accidentally left a stray blank line in ParseFuncOrColumn(). Remove it. No functional change. Author: Henson Choi Discussion: https://postgr.es/m/CAAAe_zDLBkZFXXCgR_-NuaeW+aUXUtuDoSgg-2QRz+b2g7G4BA@mail.gmail.com Backpatch-through: 19 --- src/backend/parser/parse_func.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index fb306c05112e7..9cdbaa542fe12 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -526,7 +526,6 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, errmsg("%s is not an ordered-set aggregate, so it cannot have WITHIN GROUP", NameListToString(funcname)), parser_errposition(pstate, location))); - } if (ignore_nulls != NO_NULLTREATMENT) From 8d85cb889a395f08d58e59c31a67f199f0fc25c3 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 30 Jun 2026 10:30:47 +0900 Subject: [PATCH 24/36] bufmgr: Fix race in LockBufferForCleanup() LockBufferForCleanup() acquires the exclusive content lock, checks the buffer's shared pin count, and, if other pins remain, registers itself as the BM_PIN_COUNT_WAITER before waiting for an unpin notification. Since commits 5310fac6e0f and c75ebc657ffc, however, a shared buffer pin can be released while BM_LOCKED is set, introducing the following race: - LockBufferForCleanup() observes a refcount greater than one. - Before it sets BM_PIN_COUNT_WAITER, another backend releases the last conflicting pin. - Since BM_PIN_COUNT_WAITER is not yet set, no wakeup is sent. - LockBufferForCleanup() then sets BM_PIN_COUNT_WAITER and goes to sleep, even though only its own pin remains. As a result, LockBufferForCleanup() can sleep indefinitely because the wakeup corresponding to the last conflicting unpin has already been missed. Fix this by setting BM_PIN_COUNT_WAITER while holding the buffer header lock, then rechecking the refcount before releasing the content lock. If only our pin remains, clear the waiter state and proceed without sleeping. Otherwise, wait as before. This issue was reported by buildfarm member skink, where it manifested as intermittent timeouts in 048_vacuum_horizon_floor.pl. Backpatch to v19, where commits 5310fac6e0f and c75ebc657ffc introduced the race. Reported-by: Alexander Lakhin Author: Xuneng Zhou Reviewed-by: Andres Freund Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/7685519a-0bf9-4e17-93ca-7e3aa10fa29c@gmail.com Backpatch-through: 19 --- src/backend/storage/buffer/bufmgr.c | 68 ++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d6c0cc1f6d489..f79a8fa5da21b 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -6715,24 +6715,7 @@ LockBufferForCleanup(Buffer buffer) { /* Successfully acquired exclusive lock with pincount 1 */ UnlockBufHdr(bufHdr); - - /* - * Emit the log message if recovery conflict on buffer pin was - * resolved but the startup process waited longer than - * deadlock_timeout for it. - */ - if (logged_recovery_conflict) - LogRecoveryConflict(RECOVERY_CONFLICT_BUFFERPIN, - waitStart, GetCurrentTimestamp(), - NULL, false); - - if (waiting) - { - /* reset ps display to remove the suffix if we added one */ - set_ps_display_remove_suffix(); - waiting = false; - } - return; + goto cleanup_lock_acquired; } /* Failed, so mark myself as waiting for pincount 1 */ if (buf_state & BM_PIN_COUNT_WAITER) @@ -6743,9 +6726,32 @@ LockBufferForCleanup(Buffer buffer) } bufHdr->wait_backend_pgprocno = MyProcNumber; PinCountWaitBuf = bufHdr; - UnlockBufHdrExt(bufHdr, buf_state, - BM_PIN_COUNT_WAITER, 0, - 0); + + /* + * Publish BM_PIN_COUNT_WAITER while retaining the buffer header lock. + * The shared refcount can be decremented while BM_LOCKED is set, so + * use an atomic operation that preserves concurrent refcount changes. + */ + pg_atomic_fetch_or_u64(&bufHdr->state, BM_PIN_COUNT_WAITER); + + /* + * Recheck the refcount after publishing the waiter flag, while shared + * refcount increments are still prevented by BM_LOCKED. If only our + * pin remains, the cleanup-lock condition has already been satisfied, + * so remove the waiter state and return without sleeping. + */ + buf_state = pg_atomic_read_u64(&bufHdr->state); + + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1) + { + UnlockBufHdrExt(bufHdr, buf_state, + 0, BM_PIN_COUNT_WAITER, + 0); + PinCountWaitBuf = NULL; + goto cleanup_lock_acquired; + } + + UnlockBufHdr(bufHdr); LockBuffer(buffer, BUFFER_LOCK_UNLOCK); /* Wait to be signaled by UnpinBuffer() */ @@ -6816,6 +6822,26 @@ LockBufferForCleanup(Buffer buffer) PinCountWaitBuf = NULL; /* Loop back and try again */ } + +cleanup_lock_acquired: + + /* + * Emit the log message if recovery conflict on buffer pin was resolved + * but the startup process waited longer than deadlock_timeout for it. + */ + if (logged_recovery_conflict) + LogRecoveryConflict(RECOVERY_CONFLICT_BUFFERPIN, + waitStart, GetCurrentTimestamp(), + NULL, false); + + if (waiting) + { + /* reset ps display to remove the suffix if we added one */ + set_ps_display_remove_suffix(); + waiting = false; + } + + return; } /* From cfa573cf8cbdcbf7cbcae34911bd1ee292abdd2f Mon Sep 17 00:00:00 2001 From: Noah Misch Date: Mon, 29 Jun 2026 19:41:09 -0700 Subject: [PATCH 25/36] Restore comment at appendShellString(). Commit b380a56a3f9556588a89013b765d67947d54f7d0 removed a paragraph, but two of the paragraph's three sentences remained relevant. Backpatch-through: 19 --- src/fe_utils/string_utils.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/fe_utils/string_utils.c b/src/fe_utils/string_utils.c index 38fffbd036bf7..7a762251f32bd 100644 --- a/src/fe_utils/string_utils.c +++ b/src/fe_utils/string_utils.c @@ -568,6 +568,10 @@ appendByteaLiteral(PQExpBuffer buf, const unsigned char *str, size_t length, * Append the given string to the shell command being built in the buffer, * with shell-style quoting as needed to create exactly one argument. * + * Forbid LF or CR characters, which have scant practical use beyond designing + * security breaches. The Windows command shell is unusable as a conduit for + * arguments containing LF or CR characters. + * * appendShellString() simply prints an error and dies if LF or CR appears. * appendShellStringNoError() omits those characters from the result, and * returns false if there were any. From c776550e4662385b0ebeac653ae86755008d29f3 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 30 Jun 2026 12:47:34 +0900 Subject: [PATCH 26/36] Change stat_lock.wait_time to double precision Other statistics views (pg_stat_io, pg_stat_database, etc.) use float8 for all measured-time columns, the new pg_stat_lock standing out as an outlier by using bigint. This commit aligns pg_stat_lock with the other stats views for consistency. Like pg_stat_io, the time is stored in microseconds, and is displayed in milliseconds with a conversion done when the view is queried. While on it, replace a use of "long" by PgStat_Counter, the former could overflow for large wait times where sizeof(long) is 4 bytes (aka WIN32). Bump catalog version. Author: Tatsuya Kawata Reviewed-by: Bertrand Drouvot Reviewed-by: Michael Paquier Discussion: https://postgr.es/m/CAHza6qerEiQehrbW5xaXyxvR0qJe3KBX1R4kocDz1+7Ygu8x-g@mail.gmail.com Backpatch-through: 19 --- doc/src/sgml/monitoring.sgml | 2 +- src/backend/storage/lmgr/proc.c | 9 +++++---- src/backend/utils/activity/pgstat_lock.c | 4 ++-- src/backend/utils/adt/pgstatfuncs.c | 2 +- src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_proc.dat | 2 +- src/include/pgstat.h | 5 +++-- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 08d5b8245529f..6dcf05eb7020f 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -3359,7 +3359,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage - wait_time bigint + wait_time double precision Total time spent waiting for locks of this type, in milliseconds. diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 6fa9de33e1ca2..7d01c981a1f68 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -1608,12 +1608,13 @@ ProcSleep(LOCALLOCK *locallock) TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT), GetCurrentTimestamp(), &secs, &usecs); - msecs = secs * 1000 + usecs / 1000; - usecs = usecs % 1000; - /* Increment the lock statistics counters if done waiting. */ if (myWaitStatus == PROC_WAIT_STATUS_OK) - pgstat_count_lock_waits(locallock->tag.lock.locktag_type, msecs); + pgstat_count_lock_waits(locallock->tag.lock.locktag_type, + (PgStat_Counter) secs * 1000000 + usecs); + + msecs = secs * 1000 + usecs / 1000; + usecs = usecs % 1000; if (log_lock_waits) { diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c index aec64f8fb4b63..8910a15634dd1 100644 --- a/src/backend/utils/activity/pgstat_lock.c +++ b/src/backend/utils/activity/pgstat_lock.c @@ -140,11 +140,11 @@ pgstat_count_lock_fastpath_exceeded(uint8 locktag_type) * like lock acquisitions. */ void -pgstat_count_lock_waits(uint8 locktag_type, long msecs) +pgstat_count_lock_waits(uint8 locktag_type, PgStat_Counter usecs) { Assert(locktag_type <= LOCKTAG_LAST_TYPE); PendingLockStats.stats[locktag_type].waits++; - PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs; + PendingLockStats.stats[locktag_type].wait_time += usecs; have_lockstats = true; pgstat_report_fixed = true; } diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 6f9c9c72de561..0c59df17901a5 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -1761,7 +1761,7 @@ pg_stat_get_lock(PG_FUNCTION_ARGS) values[i++] = CStringGetTextDatum(locktypename); values[i++] = Int64GetDatum(lck_stats->waits); - values[i++] = Int64GetDatum(lck_stats->wait_time); + values[i++] = Float8GetDatum(pg_stat_us_to_ms(lck_stats->wait_time)); values[i++] = Int64GetDatum(lck_stats->fastpath_exceeded); values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp); diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 635c0d9cb13e7..875a147f7533f 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202606281 +#define CATALOG_VERSION_NO 202606301 #endif diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 1a985becde32d..efe13b7866a19 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6064,7 +6064,7 @@ { oid => '6509', descr => 'statistics: per lock type statistics', proname => 'pg_stat_get_lock', prorows => '10', proretset => 't', provolatile => 'v', proparallel => 'r', prorettype => 'record', - proargtypes => '', proallargtypes => '{text,int8,int8,int8,timestamptz}', + proargtypes => '', proallargtypes => '{text,int8,float8,int8,timestamptz}', proargmodes => '{o,o,o,o,o}', proargnames => '{locktype,waits,wait_time,fastpath_exceeded,stats_reset}', prosrc => 'pg_stat_get_lock' }, diff --git a/src/include/pgstat.h b/src/include/pgstat.h index dfa2e8376382a..724966959998f 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -349,7 +349,7 @@ typedef struct PgStat_IO typedef struct PgStat_LockEntry { PgStat_Counter waits; - PgStat_Counter wait_time; /* time in milliseconds */ + PgStat_Counter wait_time; /* time in microseconds */ PgStat_Counter fastpath_exceeded; } PgStat_LockEntry; @@ -638,7 +638,8 @@ extern bool pgstat_tracks_io_op(BackendType bktype, IOObject io_object, extern void pgstat_lock_flush(bool nowait); extern void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type); -extern void pgstat_count_lock_waits(uint8 locktag_type, long msecs); +extern void pgstat_count_lock_waits(uint8 locktag_type, + PgStat_Counter usecs); extern PgStat_Lock *pgstat_fetch_stat_lock(void); /* From 7905416eef9b2ad2ff8783330d830ef2c28bef2a Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 30 Jun 2026 16:16:56 +0900 Subject: [PATCH 27/36] Use placeholders and not GUC names in error message (autovacuum) A placeholder %s is now used instead of the GUC names in the error string of this routine. This is going to be useful for a follow-up patch, where we will be able to reuse the same string, hence reducing the translation work. Based on a suggestion by me. Author: Baji Shaik Discussion: https://postgr.es/m/ajnhfw84reaXgjfO@paquier.xyz --- src/backend/postmaster/autovacuum.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index e9aaf24c1bec8..127bd9e7da3cc 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -3632,10 +3632,11 @@ check_av_worker_gucs(void) if (autovacuum_worker_slots < autovacuum_max_workers) ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"autovacuum_max_workers\" (%d) should be less than or equal to \"autovacuum_worker_slots\" (%d)", - autovacuum_max_workers, autovacuum_worker_slots), - errdetail("The server will only start up to \"autovacuum_worker_slots\" (%d) autovacuum workers at a given time.", - autovacuum_worker_slots))); + errmsg("\"%s\" (%d) should be less than or equal to \"%s\" (%d)", + "autovacuum_max_workers", autovacuum_max_workers, + "autovacuum_worker_slots", autovacuum_worker_slots), + errdetail("The server will only start up to \"%s\" (%d) autovacuum workers at a given time.", + "autovacuum_worker_slots", autovacuum_worker_slots))); } /* From dfe7d17e00665875639e905fa385231dacc57c4e Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 30 Jun 2026 16:24:34 +0900 Subject: [PATCH 28/36] Refactor pg_stat_get_lock() to use a helper function This commit extracts the tuple-building logic from pg_stat_get_lock() into a new static helper pg_stat_lock_build_tuples(). This is in preparation for a follow-up patch, to add support for backend-level lock stats, which will reuse the same helper. This change follows the pattern established by pg_stat_io_build_tuples() for IO stats and pg_stat_wal_build_tuple() for WAL stats. Author: Bertrand Drouvot Reviewed-by: Tatsuya Kawata Reviewed-by: Michael Paquier Reviewed-by: Tristan Partin Reviewed-by: Rui Zhao Discussion: https://postgr.es/m/aiAzEY+cMQb/W8yu@bdtpg --- src/backend/utils/adt/pgstatfuncs.c | 47 +++++++++++++++++++---------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 0c59df17901a5..1f9165f161605 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -1737,38 +1737,53 @@ pg_stat_get_wal(PG_FUNCTION_ARGS) wal_stats->stat_reset_timestamp)); } -Datum -pg_stat_get_lock(PG_FUNCTION_ARGS) +/* + * pg_stat_lock_build_tuples + * + * Helper routine for pg_stat_get_lock(), filling a result tuplestore with one + * tuple for each lock type. + */ +static void +pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo, + PgStat_LockEntry *lock_stats, + TimestampTz stat_reset_timestamp) { #define PG_STAT_LOCK_COLS 5 - ReturnSetInfo *rsinfo; - PgStat_Lock *lock_stats; - - InitMaterializedSRF(fcinfo, 0); - rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - - lock_stats = pgstat_fetch_stat_lock(); - for (int lcktype = 0; lcktype <= LOCKTAG_LAST_TYPE; lcktype++) { - const char *locktypename; Datum values[PG_STAT_LOCK_COLS] = {0}; bool nulls[PG_STAT_LOCK_COLS] = {0}; - PgStat_LockEntry *lck_stats = &lock_stats->stats[lcktype]; + PgStat_LockEntry *lck_stats = &lock_stats[lcktype]; int i = 0; - locktypename = LockTagTypeNames[lcktype]; - - values[i++] = CStringGetTextDatum(locktypename); + values[i++] = CStringGetTextDatum(LockTagTypeNames[lcktype]); values[i++] = Int64GetDatum(lck_stats->waits); values[i++] = Float8GetDatum(pg_stat_us_to_ms(lck_stats->wait_time)); values[i++] = Int64GetDatum(lck_stats->fastpath_exceeded); - values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp); + if (stat_reset_timestamp != 0) + values[i] = TimestampTzGetDatum(stat_reset_timestamp); + else + nulls[i] = true; Assert(i + 1 == PG_STAT_LOCK_COLS); tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); } +} + +Datum +pg_stat_get_lock(PG_FUNCTION_ARGS) +{ + ReturnSetInfo *rsinfo; + PgStat_Lock *lock_stats; + + InitMaterializedSRF(fcinfo, 0); + rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + + lock_stats = pgstat_fetch_stat_lock(); + + pg_stat_lock_build_tuples(rsinfo, lock_stats->stats, + lock_stats->stat_reset_timestamp); return (Datum) 0; } From 8c579bdc366dbca4fe9432a01408216133628c52 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 30 Jun 2026 16:59:20 +0900 Subject: [PATCH 29/36] Add backend-level lock statistics This commit adds per-backend lock statistics, providing the same information as pg_stat_lock. It is now possible to retrieve those stats (lock wait counts, wait times, and fast-path exceeded count) on a per-backend basis. This data can be retrieved with a new system function called pg_stat_get_backend_lock(), that returns one tuple per lock type based on the PID provided in input. Like pg_stat_get_backend_io(), this is useful if joined with pg_stat_activity to get a live picture of the locks behavior for each running backend. pgstat_flush_backend() gains a new flag value, able to control the flush of the lock stats. This commit is straight-forward, relying on the infrastructure provided by 9aea73fc61d4 (backend-level pgstats). Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend statistics are never written to disk. Author: Bertrand Drouvot Reviewed-by: Tatsuya Kawata Reviewed-by: Michael Paquier Reviewed-by: Tristan Partin Reviewed-by: Rui Zhao Discussion: https://postgr.es/m/aiAzEY+cMQb/W8yu@bdtpg --- doc/src/sgml/monitoring.sgml | 19 ++++++ src/backend/utils/activity/pgstat_backend.c | 72 +++++++++++++++++++++ src/backend/utils/activity/pgstat_lock.c | 4 ++ src/backend/utils/adt/pgstatfuncs.c | 29 ++++++++- src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_proc.dat | 8 +++ src/include/pgstat.h | 12 ++++ src/include/utils/pgstat_internal.h | 3 +- src/test/regress/expected/stats.out | 12 ++++ src/test/regress/sql/stats.sql | 9 +++ 10 files changed, 166 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 6dcf05eb7020f..05bd501c6822a 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -5526,6 +5526,25 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + + pg_stat_get_backend_lock + + pg_stat_get_backend_lock ( integer ) + setof record + + + Returns lock statistics about the backend with the specified + process ID. The output fields are exactly the same as the ones in the + pg_stat_lock view. + + + The function does not return lock statistics for the checkpointer, + the background writer, the startup process and the autovacuum launcher. + + + diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c index 73461c9bca590..b736b2ccc6ff3 100644 --- a/src/backend/utils/activity/pgstat_backend.c +++ b/src/backend/utils/activity/pgstat_backend.c @@ -39,6 +39,7 @@ */ static PgStat_BackendPending PendingBackendStats; static bool backend_has_iostats = false; +static bool backend_has_lockstats = false; /* * WAL usage counters saved from pgWalUsage at the previous call to @@ -86,6 +87,39 @@ pgstat_count_backend_io_op(IOObject io_object, IOContext io_context, pgstat_report_fixed = true; } +/* + * Utility routines to report lock stats for backends, kept here to avoid + * exposing PendingBackendStats to the outside world. + */ +void +pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs) +{ + if (!pgstat_tracks_backend_bktype(MyBackendType)) + return; + + Assert(locktag_type <= LOCKTAG_LAST_TYPE); + + PendingBackendStats.pending_lock.stats[locktag_type].waits++; + PendingBackendStats.pending_lock.stats[locktag_type].wait_time += usecs; + + backend_has_lockstats = true; + pgstat_report_fixed = true; +} + +void +pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type) +{ + if (!pgstat_tracks_backend_bktype(MyBackendType)) + return; + + Assert(locktag_type <= LOCKTAG_LAST_TYPE); + + PendingBackendStats.pending_lock.stats[locktag_type].fastpath_exceeded++; + + backend_has_lockstats = true; + pgstat_report_fixed = true; +} + /* * Returns statistics of a backend by proc number. */ @@ -262,6 +296,36 @@ pgstat_flush_backend_entry_wal(PgStat_EntryRef *entry_ref) prevBackendWalUsage = pgWalUsage; } +/* + * Flush out locally pending backend lock statistics. Locking is managed + * by the caller. + */ +static void +pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref) +{ + PgStatShared_Backend *shbackendent; + PgStat_PendingLock *bktype_shstats; + + if (!backend_has_lockstats) + return; + + shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats; + bktype_shstats = &shbackendent->stats.lock_stats; + + for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++) + { +#define LOCKSTAT_ACC(fld) \ + (bktype_shstats->stats[i].fld += PendingBackendStats.pending_lock.stats[i].fld) + LOCKSTAT_ACC(waits); + LOCKSTAT_ACC(wait_time); + LOCKSTAT_ACC(fastpath_exceeded); +#undef LOCKSTAT_ACC + } + + MemSet(&PendingBackendStats.pending_lock, 0, sizeof(PgStat_PendingLock)); + backend_has_lockstats = false; +} + /* * Flush out locally pending backend statistics * @@ -286,6 +350,10 @@ pgstat_flush_backend(bool nowait, uint32 flags) pgstat_backend_wal_have_pending()) has_pending_data = true; + /* Some lock data pending? */ + if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats) + has_pending_data = true; + if (!has_pending_data) return false; @@ -301,6 +369,9 @@ pgstat_flush_backend(bool nowait, uint32 flags) if (flags & PGSTAT_BACKEND_FLUSH_WAL) pgstat_flush_backend_entry_wal(entry_ref); + if (flags & PGSTAT_BACKEND_FLUSH_LOCK) + pgstat_flush_backend_entry_lock(entry_ref); + pgstat_unlock_entry(entry_ref); return false; @@ -339,6 +410,7 @@ pgstat_create_backend(ProcNumber procnum) MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending)); backend_has_iostats = false; + backend_has_lockstats = false; /* * Initialize prevBackendWalUsage with pgWalUsage so that diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c index 8910a15634dd1..c20c7599683cc 100644 --- a/src/backend/utils/activity/pgstat_lock.c +++ b/src/backend/utils/activity/pgstat_lock.c @@ -131,6 +131,8 @@ pgstat_count_lock_fastpath_exceeded(uint8 locktag_type) PendingLockStats.stats[locktag_type].fastpath_exceeded++; have_lockstats = true; pgstat_report_fixed = true; + + pgstat_count_backend_lock_fastpath_exceeded(locktag_type); } /* @@ -147,4 +149,6 @@ pgstat_count_lock_waits(uint8 locktag_type, PgStat_Counter usecs) PendingLockStats.stats[locktag_type].wait_time += usecs; have_lockstats = true; pgstat_report_fixed = true; + + pgstat_count_backend_lock_waits(locktag_type, usecs); } diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 1f9165f161605..0c6a20843a501 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -1740,8 +1740,8 @@ pg_stat_get_wal(PG_FUNCTION_ARGS) /* * pg_stat_lock_build_tuples * - * Helper routine for pg_stat_get_lock(), filling a result tuplestore with one - * tuple for each lock type. + * Helper routine for pg_stat_get_lock() and pg_stat_get_backend_lock(), + * filling a result tuplestore with one tuple for each lock type. */ static void pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo, @@ -1788,6 +1788,31 @@ pg_stat_get_lock(PG_FUNCTION_ARGS) return (Datum) 0; } +/* + * Returns lock statistics for a backend with given PID. + */ +Datum +pg_stat_get_backend_lock(PG_FUNCTION_ARGS) +{ + int pid; + ReturnSetInfo *rsinfo; + PgStat_Backend *backend_stats; + + InitMaterializedSRF(fcinfo, 0); + rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + + pid = PG_GETARG_INT32(0); + backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL); + + if (!backend_stats) + return (Datum) 0; + + pg_stat_lock_build_tuples(rsinfo, backend_stats->lock_stats.stats, + backend_stats->stat_reset_timestamp); + + return (Datum) 0; +} + /* * Returns statistics of SLRU caches. */ diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 875a147f7533f..e06eb2abbcbbc 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202606301 +#define CATALOG_VERSION_NO 202606302 #endif diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index efe13b7866a19..73bb7fbb4304f 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6092,6 +6092,14 @@ proargmodes => '{i,o,o,o,o,o,o}', proargnames => '{backend_pid,wal_records,wal_fpi,wal_bytes,wal_fpi_bytes,wal_buffers_full,stats_reset}', prosrc => 'pg_stat_get_backend_wal' }, +{ oid => '9682', descr => 'statistics: backend lock statistics', + proname => 'pg_stat_get_backend_lock', prorows => '10', proretset => 't', + provolatile => 'v', proparallel => 'r', prorettype => 'record', + proargtypes => 'int4', + proallargtypes => '{int4,text,int8,float8,int8,timestamptz}', + proargmodes => '{i,o,o,o,o,o}', + proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}', + prosrc => 'pg_stat_get_backend_lock' }, { oid => '6248', descr => 'statistics: information about WAL prefetching', proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 724966959998f..58a44857f1311 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -523,6 +523,7 @@ typedef struct PgStat_Backend TimestampTz stat_reset_timestamp; PgStat_BktypeIO io_stats; PgStat_WalCounters wal_counters; + PgStat_PendingLock lock_stats; } PgStat_Backend; /* --------- @@ -535,6 +536,12 @@ typedef struct PgStat_BackendPending * Backend statistics store the same amount of IO data as PGSTAT_KIND_IO. */ PgStat_PendingIO pending_io; + + /* + * Backend statistics store the same amount of lock data as + * PGSTAT_KIND_LOCK. + */ + PgStat_PendingLock pending_lock; } PgStat_BackendPending; /* @@ -586,6 +593,11 @@ extern void pgstat_count_backend_io_op(IOObject io_object, IOContext io_context, IOOp io_op, uint32 cnt, uint64 bytes); + +/* used by pgstat_lock.c for lock stats tracked in backends */ +extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs); +extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type); + extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber); extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid, BackendType *bktype); diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index 3ca4f45489568..b3dc3ff7d8bb7 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -705,7 +705,8 @@ extern void pgstat_archiver_snapshot_cb(void); /* flags for pgstat_flush_backend() */ #define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */ #define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */ -#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL) +#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */ +#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK) extern bool pgstat_flush_backend(bool nowait, uint32 flags); extern bool pgstat_backend_flush_cb(bool nowait); diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out index bbb1db3c43347..fa550676f8355 100644 --- a/src/test/regress/expected/stats.out +++ b/src/test/regress/expected/stats.out @@ -2019,6 +2019,10 @@ BEGIN END; $$; SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset +-- Test pg_stat_get_backend_lock() +SELECT fastpath_exceeded AS backend_fastpath_exceeded_before + FROM pg_stat_get_backend_lock(pg_backend_pid()) + WHERE locktype = 'relation' \gset -- Needs a lock on each partition SELECT count(*) FROM part_test; count @@ -2039,5 +2043,13 @@ SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE loc t (1 row) +SELECT fastpath_exceeded > :backend_fastpath_exceeded_before + FROM pg_stat_get_backend_lock(pg_backend_pid()) + WHERE locktype = 'relation'; + ?column? +---------- + t +(1 row) + DROP TABLE part_test; -- End of Stats Test diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql index 610fd21fae447..f5683302a75c0 100644 --- a/src/test/regress/sql/stats.sql +++ b/src/test/regress/sql/stats.sql @@ -998,6 +998,11 @@ $$; SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset +-- Test pg_stat_get_backend_lock() +SELECT fastpath_exceeded AS backend_fastpath_exceeded_before + FROM pg_stat_get_backend_lock(pg_backend_pid()) + WHERE locktype = 'relation' \gset + -- Needs a lock on each partition SELECT count(*) FROM part_test; @@ -1006,6 +1011,10 @@ SELECT pg_stat_force_next_flush(); SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation'; +SELECT fastpath_exceeded > :backend_fastpath_exceeded_before + FROM pg_stat_get_backend_lock(pg_backend_pid()) + WHERE locktype = 'relation'; + DROP TABLE part_test; -- End of Stats Test From cd3ad3bc03567ee120a638c840112b8865e055a8 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Tue, 30 Jun 2026 14:03:10 +0200 Subject: [PATCH 30/36] Fixes for SPI "const Datum *" use Fixup for commit 8a27d418f8f, which converted many functions to use "const Datum *" instead of "Datum *", including some SPI functions. For SPI_cursor_open(), the code was updated but not the documentation. For SPI_cursor_open_with_args(), the documentation was updated but not the code. (Possibly, these two were confused with each other.) Also, SPI_execp() and SPI_modifytuple() were not updated, even though they are closely related to the functions touched by the previous commit and now look inconsistent. Fix all these. Reviewed-by: Tom Lane Discussion: https://www.postgresql.org/message-id/flat/86b5162f-c472-40fa-997b-0450dece1dec%40eisentraut.org --- doc/src/sgml/spi.sgml | 6 +++--- src/backend/executor/spi.c | 6 +++--- src/include/executor/spi.h | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml index e30d0962ae761..179cceb8da947 100644 --- a/doc/src/sgml/spi.sgml +++ b/doc/src/sgml/spi.sgml @@ -2086,7 +2086,7 @@ int SPI_execute_plan_with_paramlist(SPIPlanPtr plan, -int SPI_execp(SPIPlanPtr plan, Datum * values, const char * nulls, long count) +int SPI_execp(SPIPlanPtr plan, const Datum * values, const char * nulls, long count) @@ -2191,7 +2191,7 @@ int SPI_execp(SPIPlanPtr plan, Datum * values< Portal SPI_cursor_open(const char * name, SPIPlanPtr plan, - Datum * values, const char * nulls, + const Datum * values, const char * nulls, bool read_only) @@ -4694,7 +4694,7 @@ HeapTupleHeader SPI_returntuple(HeapTuple row, TupleDesc HeapTuple SPI_modifytuple(Relation rel, HeapTuple row, int ncols, - int * colnum, Datum * values, const char * nulls) + int * colnum, const Datum * values, const char * nulls) diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 52f3b11301c55..4e52542a3d17f 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -702,7 +702,7 @@ SPI_execute_plan(SPIPlanPtr plan, const Datum *Values, const char *Nulls, /* Obsolete version of SPI_execute_plan */ int -SPI_execp(SPIPlanPtr plan, Datum *Values, const char *Nulls, long tcount) +SPI_execp(SPIPlanPtr plan, const Datum *Values, const char *Nulls, long tcount) { return SPI_execute_plan(plan, Values, Nulls, false, tcount); } @@ -1105,7 +1105,7 @@ SPI_returntuple(HeapTuple tuple, TupleDesc tupdesc) HeapTuple SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, int *attnum, - Datum *Values, const char *Nulls) + const Datum *Values, const char *Nulls) { MemoryContext oldcxt; HeapTuple mtuple; @@ -1473,7 +1473,7 @@ Portal SPI_cursor_open_with_args(const char *name, const char *src, int nargs, Oid *argtypes, - Datum *Values, const char *Nulls, + const Datum *Values, const char *Nulls, bool read_only, int cursorOptions) { Portal result; diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h index f4985cb715d9f..fd79f86a98c23 100644 --- a/src/include/executor/spi.h +++ b/src/include/executor/spi.h @@ -119,7 +119,7 @@ extern int SPI_execute_plan_with_paramlist(SPIPlanPtr plan, ParamListInfo params, bool read_only, long tcount); extern int SPI_exec(const char *src, long tcount); -extern int SPI_execp(SPIPlanPtr plan, Datum *Values, const char *Nulls, +extern int SPI_execp(SPIPlanPtr plan, const Datum *Values, const char *Nulls, long tcount); extern int SPI_execute_snapshot(SPIPlanPtr plan, const Datum *Values, const char *Nulls, @@ -155,7 +155,7 @@ extern CachedPlan *SPI_plan_get_cached_plan(SPIPlanPtr plan); extern HeapTuple SPI_copytuple(HeapTuple tuple); extern HeapTupleHeader SPI_returntuple(HeapTuple tuple, TupleDesc tupdesc); extern HeapTuple SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, - int *attnum, Datum *Values, const char *Nulls); + int *attnum, const Datum *Values, const char *Nulls); extern int SPI_fnumber(TupleDesc tupdesc, const char *fname); extern char *SPI_fname(TupleDesc tupdesc, int fnumber); extern char *SPI_getvalue(HeapTuple tuple, TupleDesc tupdesc, int fnumber); @@ -176,7 +176,7 @@ extern Portal SPI_cursor_open(const char *name, SPIPlanPtr plan, extern Portal SPI_cursor_open_with_args(const char *name, const char *src, int nargs, Oid *argtypes, - Datum *Values, const char *Nulls, + const Datum *Values, const char *Nulls, bool read_only, int cursorOptions); extern Portal SPI_cursor_open_with_paramlist(const char *name, SPIPlanPtr plan, ParamListInfo params, bool read_only); From b1c41398e48ca7d38a46c901dc93872c968b227c Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Tue, 30 Jun 2026 15:43:56 +0200 Subject: [PATCH 31/36] Make SPI_prepare argtypes argument const This changes the argtypes argument of SPI_prepare(), SPI_prepare_cursor(), SPI_cursor_open_with_args(), and SPI_execute_with_args() from Oid *argtypes to const Oid *argtypes. The underlying functions were already receptive to that, so this doesn't require any significant changes beyond the function signatures and some internal variables. Commit 28972b6fc3dc recently introduced a case where a const had to be cast away before calling these functions. This is fixed here. In passing, make a very similar const addition to SPI_modifytuple(). Reviewed-by: Tom Lane Reviewed-by: Ewan Young Discussion: https://www.postgresql.org/message-id/flat/86b5162f-c472-40fa-997b-0450dece1dec%40eisentraut.org --- contrib/postgres_fdw/postgres_fdw.c | 6 +++--- doc/src/sgml/spi.sgml | 10 +++++----- src/backend/executor/spi.c | 28 +++++++++++++++++----------- src/backend/utils/adt/ri_triggers.c | 4 ++-- src/backend/utils/cache/plancache.c | 2 +- src/include/executor/spi.h | 10 +++++----- src/include/executor/spi_priv.h | 2 +- src/include/utils/plancache.h | 2 +- 8 files changed, 35 insertions(+), 29 deletions(-) diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 6dbae583ecc4f..6fa45773c30d6 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -6164,12 +6164,12 @@ import_fetched_statistics(const char *schemaname, Assert(PQntuples(remstats->att) >= 1); attimport_plan = SPI_prepare(attimport_sql, ATTIMPORT_SQL_NUM_FIELDS, - (Oid *) attimport_argtypes); + attimport_argtypes); if (attimport_plan == NULL) elog(ERROR, "failed to prepare attimport_sql query"); attclear_plan = SPI_prepare(attclear_sql, ATTCLEAR_SQL_NUM_FIELDS, - (Oid *) attclear_argtypes); + attclear_argtypes); if (attclear_plan == NULL) elog(ERROR, "failed to prepare attclear_sql query"); @@ -6247,7 +6247,7 @@ import_fetched_statistics(const char *schemaname, spirc = SPI_execute_with_args(relimport_sql, RELIMPORT_SQL_NUM_FIELDS, - (Oid *) relimport_argtypes, + relimport_argtypes, values, nulls, false, 1); if (spirc != SPI_OK_SELECT) elog(ERROR, "failed to execute relimport_sql query for foreign table \"%s.%s\"", diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml index 179cceb8da947..5846fcceb8976 100644 --- a/doc/src/sgml/spi.sgml +++ b/doc/src/sgml/spi.sgml @@ -845,7 +845,7 @@ int SPI_execute_extended(const char *command, int SPI_execute_with_args(const char *command, - int nargs, Oid *argtypes, + int nargs, const Oid *argtypes, const Datum *values, const char *nulls, bool read_only, long count) @@ -997,7 +997,7 @@ int SPI_execute_with_args(const char *command, -SPIPlanPtr SPI_prepare(const char * command, int nargs, Oid * argtypes) +SPIPlanPtr SPI_prepare(const char * command, int nargs, const Oid * argtypes) @@ -1160,7 +1160,7 @@ SPIPlanPtr SPI_prepare(const char * command, int SPIPlanPtr SPI_prepare_cursor(const char * command, int nargs, - Oid * argtypes, int cursorOptions) + const Oid * argtypes, int cursorOptions) @@ -2316,7 +2316,7 @@ Portal SPI_cursor_open(const char * name, SPIPlanPtr Portal SPI_cursor_open_with_args(const char *name, const char *command, - int nargs, Oid *argtypes, + int nargs, const Oid *argtypes, const Datum *values, const char *nulls, bool read_only, int cursorOptions) @@ -4694,7 +4694,7 @@ HeapTupleHeader SPI_returntuple(HeapTuple row, TupleDesc HeapTuple SPI_modifytuple(Relation rel, HeapTuple row, int ncols, - int * colnum, const Datum * values, const char * nulls) + const int * colnum, const Datum * values, const char * nulls) diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 4e52542a3d17f..23a8395766c30 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -68,7 +68,7 @@ static int _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options, Snapshot snapshot, Snapshot crosscheck_snapshot, bool fire_triggers); -static ParamListInfo _SPI_convert_params(int nargs, Oid *argtypes, +static ParamListInfo _SPI_convert_params(int nargs, const Oid *argtypes, const Datum *Values, const char *Nulls); static int _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount); @@ -811,7 +811,7 @@ SPI_execute_snapshot(SPIPlanPtr plan, */ int SPI_execute_with_args(const char *src, - int nargs, Oid *argtypes, + int nargs, const Oid *argtypes, const Datum *Values, const char *Nulls, bool read_only, long tcount) { @@ -858,13 +858,13 @@ SPI_execute_with_args(const char *src, } SPIPlanPtr -SPI_prepare(const char *src, int nargs, Oid *argtypes) +SPI_prepare(const char *src, int nargs, const Oid *argtypes) { return SPI_prepare_cursor(src, nargs, argtypes, 0); } SPIPlanPtr -SPI_prepare_cursor(const char *src, int nargs, Oid *argtypes, +SPI_prepare_cursor(const char *src, int nargs, const Oid *argtypes, int cursorOptions) { _SPI_plan plan; @@ -1104,7 +1104,7 @@ SPI_returntuple(HeapTuple tuple, TupleDesc tupdesc) } HeapTuple -SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, int *attnum, +SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, const int *attnum, const Datum *Values, const char *Nulls) { MemoryContext oldcxt; @@ -1472,7 +1472,7 @@ SPI_cursor_open(const char *name, SPIPlanPtr plan, Portal SPI_cursor_open_with_args(const char *name, const char *src, - int nargs, Oid *argtypes, + int nargs, const Oid *argtypes, const Datum *Values, const char *Nulls, bool read_only, int cursorOptions) { @@ -2846,7 +2846,7 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options, * Convert arrays of query parameters to form wanted by planner and executor */ static ParamListInfo -_SPI_convert_params(int nargs, Oid *argtypes, +_SPI_convert_params(int nargs, const Oid *argtypes, const Datum *Values, const char *Nulls) { ParamListInfo paramLI; @@ -3170,8 +3170,11 @@ _SPI_make_plan_non_temp(SPIPlanPtr plan) newplan->nargs = plan->nargs; if (plan->nargs > 0) { - newplan->argtypes = palloc_array(Oid, plan->nargs); - memcpy(newplan->argtypes, plan->argtypes, plan->nargs * sizeof(Oid)); + Oid *newplan_argtypes; + + newplan_argtypes = palloc_array(Oid, plan->nargs); + memcpy(newplan_argtypes, plan->argtypes, plan->nargs * sizeof(Oid)); + newplan->argtypes = newplan_argtypes; } else newplan->argtypes = NULL; @@ -3235,8 +3238,11 @@ _SPI_save_plan(SPIPlanPtr plan) newplan->nargs = plan->nargs; if (plan->nargs > 0) { - newplan->argtypes = palloc_array(Oid, plan->nargs); - memcpy(newplan->argtypes, plan->argtypes, plan->nargs * sizeof(Oid)); + Oid *newplan_argtypes; + + newplan_argtypes = palloc_array(Oid, plan->nargs); + memcpy(newplan_argtypes, plan->argtypes, plan->nargs * sizeof(Oid)); + newplan->argtypes = newplan_argtypes; } else newplan->argtypes = NULL; diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index bf54f9b459243..627a9fb38ea37 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -308,7 +308,7 @@ static RI_ConstraintInfo *ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk); static RI_ConstraintInfo *ri_LoadConstraintInfo(Oid constraintOid); static Oid get_ri_constraint_root(Oid constrOid); -static SPIPlanPtr ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes, +static SPIPlanPtr ri_PlanCheck(const char *querystr, int nargs, const Oid *argtypes, RI_QueryKey *qkey, Relation fk_rel, Relation pk_rel); static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo, RI_QueryKey *qkey, SPIPlanPtr qplan, @@ -2605,7 +2605,7 @@ InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, * Prepare execution plan for a query to enforce an RI restriction */ static SPIPlanPtr -ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes, +ri_PlanCheck(const char *querystr, int nargs, const Oid *argtypes, RI_QueryKey *qkey, Relation fk_rel, Relation pk_rel) { SPIPlanPtr qplan; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 698e7c1aa220f..26f1bd6451534 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -393,7 +393,7 @@ void CompleteCachedPlan(CachedPlanSource *plansource, List *querytree_list, MemoryContext querytree_context, - Oid *param_types, + const Oid *param_types, int num_params, ParserSetupHook parserSetup, void *parserSetupArg, diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h index fd79f86a98c23..c7a3b7abb2609 100644 --- a/src/include/executor/spi.h +++ b/src/include/executor/spi.h @@ -127,11 +127,11 @@ extern int SPI_execute_snapshot(SPIPlanPtr plan, Snapshot crosscheck_snapshot, bool read_only, bool fire_triggers, long tcount); extern int SPI_execute_with_args(const char *src, - int nargs, Oid *argtypes, + int nargs, const Oid *argtypes, const Datum *Values, const char *Nulls, bool read_only, long tcount); -extern SPIPlanPtr SPI_prepare(const char *src, int nargs, Oid *argtypes); -extern SPIPlanPtr SPI_prepare_cursor(const char *src, int nargs, Oid *argtypes, +extern SPIPlanPtr SPI_prepare(const char *src, int nargs, const Oid *argtypes); +extern SPIPlanPtr SPI_prepare_cursor(const char *src, int nargs, const Oid *argtypes, int cursorOptions); extern SPIPlanPtr SPI_prepare_extended(const char *src, const SPIPrepareOptions *options); @@ -155,7 +155,7 @@ extern CachedPlan *SPI_plan_get_cached_plan(SPIPlanPtr plan); extern HeapTuple SPI_copytuple(HeapTuple tuple); extern HeapTupleHeader SPI_returntuple(HeapTuple tuple, TupleDesc tupdesc); extern HeapTuple SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, - int *attnum, const Datum *Values, const char *Nulls); + const int *attnum, const Datum *Values, const char *Nulls); extern int SPI_fnumber(TupleDesc tupdesc, const char *fname); extern char *SPI_fname(TupleDesc tupdesc, int fnumber); extern char *SPI_getvalue(HeapTuple tuple, TupleDesc tupdesc, int fnumber); @@ -175,7 +175,7 @@ extern Portal SPI_cursor_open(const char *name, SPIPlanPtr plan, const Datum *Values, const char *Nulls, bool read_only); extern Portal SPI_cursor_open_with_args(const char *name, const char *src, - int nargs, Oid *argtypes, + int nargs, const Oid *argtypes, const Datum *Values, const char *Nulls, bool read_only, int cursorOptions); extern Portal SPI_cursor_open_with_paramlist(const char *name, SPIPlanPtr plan, diff --git a/src/include/executor/spi_priv.h b/src/include/executor/spi_priv.h index 240383b97b9db..9fb82ad098422 100644 --- a/src/include/executor/spi_priv.h +++ b/src/include/executor/spi_priv.h @@ -97,7 +97,7 @@ typedef struct _SPI_plan RawParseMode parse_mode; /* raw_parser() mode */ int cursor_options; /* Cursor options used for planning */ int nargs; /* number of plan arguments */ - Oid *argtypes; /* Argument types (NULL if nargs is 0) */ + const Oid *argtypes; /* Argument types (NULL if nargs is 0) */ ParserSetupHook parserSetup; /* alternative parameter spec method */ void *parserSetupArg; } _SPI_plan; diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h index 7a4a85c803835..a0355e79c2835 100644 --- a/src/include/utils/plancache.h +++ b/src/include/utils/plancache.h @@ -214,7 +214,7 @@ extern CachedPlanSource *CreateOneShotCachedPlan(RawStmt *raw_parse_tree, extern void CompleteCachedPlan(CachedPlanSource *plansource, List *querytree_list, MemoryContext querytree_context, - Oid *param_types, + const Oid *param_types, int num_params, ParserSetupHook parserSetup, void *parserSetupArg, From ae27a41e0c7f1d1c451aff230cac32fc7cdb1fee Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Tue, 30 Jun 2026 10:57:54 -0500 Subject: [PATCH 32/36] Remove pg_spin_delay(). This code appears to be an artifact from commit b64d92f1a5 that was never used for anything. Reviewed-by: Corey Huinker Discussion: https://postgr.es/m/afouZUH_eUkIj4i4%40nathan --- src/include/port/atomics.h | 5 --- src/include/port/atomics/arch-x86.h | 54 ----------------------------- src/include/port/atomics/generic.h | 6 ---- 3 files changed, 65 deletions(-) diff --git a/src/include/port/atomics.h b/src/include/port/atomics.h index d8b1d20fe60fa..c50d95d29e2e0 100644 --- a/src/include/port/atomics.h +++ b/src/include/port/atomics.h @@ -154,11 +154,6 @@ #define pg_read_barrier() pg_read_barrier_impl() #define pg_write_barrier() pg_write_barrier_impl() -/* - * Spinloop delay - Allow CPU to relax in busy loops - */ -#define pg_spin_delay() pg_spin_delay_impl() - /* * pg_atomic_init_flag - initialize atomic flag. * diff --git a/src/include/port/atomics/arch-x86.h b/src/include/port/atomics/arch-x86.h index 8cfe402c33935..88bba0e5f5cdb 100644 --- a/src/include/port/atomics/arch-x86.h +++ b/src/include/port/atomics/arch-x86.h @@ -76,60 +76,6 @@ typedef struct pg_atomic_uint64 } pg_atomic_uint64; #endif /* __x86_64__ */ -#endif /* defined(__GNUC__) || defined(__INTEL_COMPILER) */ - -#if !defined(PG_HAVE_SPIN_DELAY) -/* - * This sequence is equivalent to the PAUSE instruction ("rep" is - * ignored by old IA32 processors if the following instruction is - * not a string operation); the IA-32 Architecture Software - * Developer's Manual, Vol. 3, Section 7.7.2 describes why using - * PAUSE in the inner loop of a spin lock is necessary for good - * performance: - * - * The PAUSE instruction improves the performance of IA-32 - * processors supporting Hyper-Threading Technology when - * executing spin-wait loops and other routines where one - * thread is accessing a shared lock or semaphore in a tight - * polling loop. When executing a spin-wait loop, the - * processor can suffer a severe performance penalty when - * exiting the loop because it detects a possible memory order - * violation and flushes the core processor's pipeline. The - * PAUSE instruction provides a hint to the processor that the - * code sequence is a spin-wait loop. The processor uses this - * hint to avoid the memory order violation and prevent the - * pipeline flush. In addition, the PAUSE instruction - * de-pipelines the spin-wait loop to prevent it from - * consuming execution resources excessively. - */ -#if defined(__GNUC__) || defined(__INTEL_COMPILER) -#define PG_HAVE_SPIN_DELAY -static inline void -pg_spin_delay_impl(void) -{ - __asm__ __volatile__(" rep; nop \n"); -} -#elif defined(_MSC_VER) && defined(__x86_64__) -#define PG_HAVE_SPIN_DELAY -static __forceinline void -pg_spin_delay_impl(void) -{ - _mm_pause(); -} -#elif defined(_MSC_VER) -#define PG_HAVE_SPIN_DELAY -static __forceinline void -pg_spin_delay_impl(void) -{ - /* See comment for gcc code. Same code, MASM syntax */ - __asm rep nop; -} -#endif -#endif /* !defined(PG_HAVE_SPIN_DELAY) */ - - -#if defined(__GNUC__) || defined(__INTEL_COMPILER) - #define PG_HAVE_ATOMIC_TEST_SET_FLAG static inline bool pg_atomic_test_set_flag_impl(volatile pg_atomic_flag *ptr) diff --git a/src/include/port/atomics/generic.h b/src/include/port/atomics/generic.h index fd64b6cbd8681..daa772e9a6d00 100644 --- a/src/include/port/atomics/generic.h +++ b/src/include/port/atomics/generic.h @@ -28,12 +28,6 @@ # define pg_write_barrier_impl pg_memory_barrier_impl #endif -#ifndef PG_HAVE_SPIN_DELAY -#define PG_HAVE_SPIN_DELAY -#define pg_spin_delay_impl() ((void)0) -#endif - - /* provide fallback */ #if !defined(PG_HAVE_ATOMIC_FLAG_SUPPORT) && defined(PG_HAVE_ATOMIC_U32_SUPPORT) #define PG_HAVE_ATOMIC_FLAG_SUPPORT From 2ef57e636fc97528a37515673f5f56a1fcf97186 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 30 Jun 2026 12:21:06 -0400 Subject: [PATCH 33/36] Clean up inconsistencies in CPU-identification macros. In various places we depend on compiler-defined macros like __x86_64__ to guard CPU-type-specific code. However, those macros aren't very well standardized; in particular, it emerges that MSVC doesn't define any of the ones gcc does, but has its own. We were not coping with that consistently, with the result that we're missing some useful CPU-dependent optimizations in MSVC builds. There are also some places that are checking randomly-different spellings that may have been the only ones recognized by some old compilers, but we weren't doing that consistently either. Let's standardize on using gcc's long-form spellings (with trailing underscores), after putting a stanza into c.h that ensures that these spellings are defined even when the compiler provides some other one. I put an "#else #error" branch into the c.h addition so that we'll get an error if the compiler provides none of the symbols we're expecting. That might be best removed in the end, since it might annoy people trying to port to some new CPU type. But for testing this it seems like a good idea, in case we've missed some common variant spelling. In addition to enabling some optimizations we previously missed on MSVC, this cleans up a thinko. Several places used "_M_X64" in the apparent belief that that's MSVC's equivalent to __x86_64__, but it's not: it will also get defined on some but not all ARM64 builds. Also, guard the x86_feature_available() stuff in pg_cpu.[hc] with #if defined(__x86_64__) || defined(__i386__) which seems like a more natural way of specifying what it applies to. This builds on some previous work by Thomas Munro, but it requires much less code churn because it re-uses gcc's names for the CPU-type macros instead of inventing our own. Author: Tom Lane Discussion: https://postgr.es/m/CA+hUKGL8Hs-phHPugrWM=5dAkcT897rXyazYzLw-Szxnzgx-rA@mail.gmail.com Discussion: https://postgr.es/m/3035145.1780503430@sss.pgh.pa.us --- src/common/d2s.c | 2 +- src/include/c.h | 58 +++++++++++++++++++++++++++- src/include/port/atomics.h | 6 +-- src/include/port/atomics/arch-x86.h | 4 +- src/include/port/pg_bitutils.h | 4 +- src/include/port/pg_cpu.h | 4 +- src/include/portability/instr_time.h | 2 +- src/include/storage/s_lock.h | 10 ++--- src/port/pg_cpu_x86.c | 6 +-- 9 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/common/d2s.c b/src/common/d2s.c index 34e7cd33bf9f5..0a9cb83fc4929 100644 --- a/src/common/d2s.c +++ b/src/common/d2s.c @@ -53,7 +53,7 @@ * alignment concerns that apply elsewhere. */ #if !defined(HAVE_INT128) && defined(_MSC_VER) \ - && !defined(RYU_ONLY_64_BIT_OPS) && defined(_M_X64) + && !defined(RYU_ONLY_64_BIT_OPS) && defined(__x86_64__) #define HAS_64_BIT_INTRINSICS #endif diff --git a/src/include/c.h b/src/include/c.h index f32989a633105..ef4dc5d1e634a 100644 --- a/src/include/c.h +++ b/src/include/c.h @@ -106,6 +106,62 @@ extern "C++" * ---------------------------------------------------------------- */ +/* + * Not all compilers follow gcc's names of macros for particular target + * architectures. Let's standardize on gcc's names (with trailing __), + * and cause those to become defined here if they are not already. + * + * Note: while this list is alphabetical, it's necessary to check _M_ARM64 + * before _M_AMD64, because Microsoft's ARM64EC environment defines both. + */ +#if defined(__arm__) || defined(__arm) +#ifndef __arm__ +#define __arm__ 1 +#endif +#elif defined(__aarch64__) || defined(_M_ARM64) +#ifndef __aarch64__ +#define __aarch64__ 1 +#endif +#elif defined(__loongarch64__) || defined(__loongarch64) +#ifndef __loongarch64__ +#define __loongarch64__ 1 +#endif +#elif defined(__mips__) +/* no work */ +#elif defined(__mips64__) +/* no work */ +#elif defined(__powerpc__) || defined(__ppc__) +#ifndef __powerpc__ +#define __powerpc__ 1 +#endif +#elif defined(__powerpc64__) || defined(__ppc64__) +#ifndef __powerpc64__ +#define __powerpc64__ 1 +#endif +#elif defined(__riscv__) +/* no work */ +#elif defined(__riscv64__) +/* no work */ +#elif defined(__s390__) +/* no work */ +#elif defined(__s390x__) +/* no work */ +#elif defined(__sparc__) || defined(__sparc) +#ifndef __sparc__ +#define __sparc__ 1 +#endif +#elif defined(__i386__) || defined (__i386) || defined(_M_IX86) +#ifndef __i386__ +#define __i386__ 1 +#endif +#elif defined(__x86_64__) || defined(__x86_64) || defined (__amd64) || defined(_M_AMD64) +#ifndef __x86_64__ +#define __x86_64__ 1 +#endif +#else +#error "cannot identify target architecture" +#endif + /* * Disable "inline" if PG_FORCE_DISABLE_INLINE is defined. * This is used to work around compiler bugs and might also be useful for @@ -1337,7 +1393,7 @@ typedef struct PGAlignedXLogBlock PGAlignedXLogBlock; * SSE2 instructions are part of the spec for the 64-bit x86 ISA. We assume * that compilers targeting this architecture understand SSE2 intrinsics. */ -#if (defined(__x86_64__) || defined(_M_AMD64)) +#if defined(__x86_64__) #define USE_SSE2 #else /* ! x86_64 */ diff --git a/src/include/port/atomics.h b/src/include/port/atomics.h index c50d95d29e2e0..a605ea81d0760 100644 --- a/src/include/port/atomics.h +++ b/src/include/port/atomics.h @@ -63,11 +63,11 @@ * compiler barrier. * */ -#if defined(__arm__) || defined(__arm) || defined(__aarch64__) +#if defined(__arm__) || defined(__aarch64__) #include "port/atomics/arch-arm.h" -#elif defined(__i386__) || defined(__i386) || defined(__x86_64__) +#elif defined(__i386__) || defined(__x86_64__) #include "port/atomics/arch-x86.h" -#elif defined(__ppc__) || defined(__powerpc__) || defined(__ppc64__) || defined(__powerpc64__) +#elif defined(__powerpc__) || defined(__powerpc64__) #include "port/atomics/arch-ppc.h" #endif diff --git a/src/include/port/atomics/arch-x86.h b/src/include/port/atomics/arch-x86.h index 88bba0e5f5cdb..231831dcf5979 100644 --- a/src/include/port/atomics/arch-x86.h +++ b/src/include/port/atomics/arch-x86.h @@ -32,7 +32,7 @@ */ #if defined(__GNUC__) || defined(__INTEL_COMPILER) -#if defined(__i386__) || defined(__i386) +#if defined(__i386__) #define pg_memory_barrier_impl() \ __asm__ __volatile__ ("lock; addl $0,0(%%esp)" : : : "memory", "cc") #elif defined(__x86_64__) @@ -184,6 +184,6 @@ pg_atomic_fetch_add_u64_impl(volatile pg_atomic_uint64 *ptr, int64 add_) /* * 8 byte reads / writes have single-copy atomicity on all x86-64 cpus. */ -#if defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) /* gcc, msvc */ +#if defined(__x86_64__) #define PG_HAVE_8BYTE_SINGLE_COPY_ATOMICITY #endif /* 8 byte single-copy atomicity */ diff --git a/src/include/port/pg_bitutils.h b/src/include/port/pg_bitutils.h index 7a00d197013ba..2864ba431db1d 100644 --- a/src/include/port/pg_bitutils.h +++ b/src/include/port/pg_bitutils.h @@ -82,7 +82,7 @@ pg_leftmost_one_pos64(uint64 word) #error "cannot find integer type of the same size as uint64_t" #endif -#elif defined(_MSC_VER) && (defined(_M_AMD64) || defined(_M_ARM64)) +#elif defined(_MSC_VER) && (defined(__x86_64__) || defined(__aarch64__)) unsigned long result; bool non_zero; @@ -155,7 +155,7 @@ pg_rightmost_one_pos64(uint64 word) #error "cannot find integer type of the same size as uint64_t" #endif -#elif defined(_MSC_VER) && (defined(_M_AMD64) || defined(_M_ARM64)) +#elif defined(_MSC_VER) && (defined(__x86_64__) || defined(__aarch64__)) unsigned long result; bool non_zero; diff --git a/src/include/port/pg_cpu.h b/src/include/port/pg_cpu.h index 566ed7a16e393..22aaa1aa5bcc5 100644 --- a/src/include/port/pg_cpu.h +++ b/src/include/port/pg_cpu.h @@ -13,7 +13,7 @@ #ifndef PG_CPU_H #define PG_CPU_H -#if defined(USE_SSE2) || defined(__i386__) +#if defined(__x86_64__) || defined(__i386__) typedef enum X86FeatureId { @@ -58,6 +58,6 @@ x86_feature_available(X86FeatureId feature) extern uint32 x86_tsc_frequency_khz(char *source, size_t source_size); -#endif /* defined(USE_SSE2) || defined(__i386__) */ +#endif /* defined(__x86_64__) || defined(__i386__) */ #endif /* PG_CPU_H */ diff --git a/src/include/portability/instr_time.h b/src/include/portability/instr_time.h index 655f8737b6ff1..826cc202847fc 100644 --- a/src/include/portability/instr_time.h +++ b/src/include/portability/instr_time.h @@ -95,7 +95,7 @@ typedef struct instr_time * PG_INSTR_TSC_CLOCK controls whether the TSC clock source is compiled in, and * potentially used based on timing_tsc_enabled. */ -#if defined(__x86_64__) || defined(_M_X64) +#if defined(__x86_64__) #define PG_INSTR_TICKS_TO_NS 1 #define PG_INSTR_TSC_CLOCK 1 #elif defined(WIN32) diff --git a/src/include/storage/s_lock.h b/src/include/storage/s_lock.h index c9e5251199005..96fcaaac01e76 100644 --- a/src/include/storage/s_lock.h +++ b/src/include/storage/s_lock.h @@ -247,7 +247,7 @@ spin_delay(void) * We use the int-width variant of the builtin because it works on more chips * than other widths. */ -#if defined(__arm__) || defined(__arm) || defined(__aarch64__) +#if defined(__arm__) || defined(__aarch64__) #ifdef HAVE_GCC__SYNC_INT32_TAS #define HAS_TEST_AND_SET @@ -287,7 +287,7 @@ spin_delay(void) #endif /* __aarch64__ */ #endif /* HAVE_GCC__SYNC_INT32_TAS */ -#endif /* __arm__ || __arm || __aarch64__ */ +#endif /* __arm__ || __aarch64__ */ /* S/390 and S/390x Linux (32- and 64-bit zSeries) */ @@ -391,7 +391,7 @@ do \ /* PowerPC */ -#if defined(__ppc__) || defined(__powerpc__) || defined(__ppc64__) || defined(__powerpc64__) +#if defined(__powerpc__) || defined(__powerpc64__) #define HAS_TEST_AND_SET typedef unsigned int slock_t; @@ -602,7 +602,7 @@ typedef LONG slock_t; #define SPIN_DELAY() spin_delay() -#ifdef _M_ARM64 +#ifdef __aarch64__ static __forceinline void spin_delay(void) { @@ -633,7 +633,7 @@ spin_delay(void) #include -#ifdef _M_ARM64 +#ifdef __aarch64__ /* _ReadWriteBarrier() is insufficient on non-TSO architectures. */ #pragma intrinsic(_InterlockedExchange) diff --git a/src/port/pg_cpu_x86.c b/src/port/pg_cpu_x86.c index 0405ba19f6f53..b050677f717d3 100644 --- a/src/port/pg_cpu_x86.c +++ b/src/port/pg_cpu_x86.c @@ -19,7 +19,7 @@ #include "postgres_fe.h" #endif -#if defined(USE_SSE2) || defined(__i386__) +#if defined(__x86_64__) || defined(__i386__) #ifdef _MSC_VER #include @@ -287,10 +287,10 @@ x86_hypervisor_tsc_frequency_khz(void) return 0; } -#else /* defined(USE_SSE2) || defined(__i386__) */ +#else /* defined(__x86_64__) || defined(__i386__) */ /* prevent linker complaints about empty module */ extern int pg_cpu_x86_dummy_variable; int pg_cpu_x86_dummy_variable = 0; -#endif /* ! (USE_SSE2 || __i386__) */ +#endif /* ! (__x86_64__ || __i386__) */ From 57f19774d6c88f501c835a7772a71ca5ba2bc163 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Tue, 30 Jun 2026 22:29:43 +0300 Subject: [PATCH 34/36] doc: clarify MERGE PARTITIONS adjacency requirement The existing description says the ranges of merged range-partitions "must be adjacent" only under the heading "If the DEFAULT partition is not in the list of merged partitions". That could be misread as a restriction tied to the presence of a default partition. In fact, merging non-adjacent ranges is rejected regardless of whether the partitioned table has a default partition; spell that out explicitly. Also, this commit removes a small redundancy in the documentation sentence stating that "merged range-partitions" are "to be merged". Reported-by: Justin Pryzby Discussion: https://postgr.es/m/aj6BPoziSb-F8aJz%40pryzbyj2023 Reported-by: Pavel Borisov Backpatch-through: 19 --- doc/src/sgml/ref/alter_table.sgml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 6dd518752c003..ff7071bef5b4d 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -1206,7 +1206,8 @@ WITH ( MODULUS numeric_literal, REM For range-partitioned tables, the ranges of merged partitions - must be adjacent in order to be merged. + must be adjacent; this applies even if the partitioned table + has no default partition. The partition bounds of merged partitions are combined to form the new partition bound for partition_name. From 1de468099d27f44c1998c9c2251cd2aefcfab524 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 30 Jun 2026 17:21:23 -0400 Subject: [PATCH 35/36] Disallow set-returning functions within window OVER clauses. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We previously allowed this, but it leads to odd behaviors, basically because putting a SRF there is inconsistent with the principle that a window function doesn't change the number of rows in the query result. There doesn't seem to be a strong reason to try to make such cases behave consistently. Users should put their SRFs in lateral FROM clauses instead. This issue has been sitting on the back burner for multiple years now, partially because it didn't seem wise to back-patch such a change. Let's squeeze it into v19 before it's too late. Bug: #17502 Bug: #19535 Reported-by: Daniel Farkaš Reported-by: Qifan Liu Author: Tom Lane Reviewed-by: David Rowley Discussion: https://postgr.es/m/17502-281a7aaacfaa872a@postgresql.org Discussion: https://postgr.es/m/19535-376081d7cc07c86d@postgresql.org Backpatch-through: 19 --- src/backend/parser/parse_func.c | 3 --- src/test/regress/expected/tsrf.out | 25 +++++++++++++++---------- src/test/regress/sql/tsrf.sql | 10 +++++++--- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 9cdbaa542fe12..a9b6be7203b3d 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2700,9 +2700,6 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) break; case EXPR_KIND_WINDOW_PARTITION: case EXPR_KIND_WINDOW_ORDER: - /* okay, these are effectively GROUP BY/ORDER BY */ - pstate->p_hasTargetSRFs = true; - break; case EXPR_KIND_WINDOW_FRAME_RANGE: case EXPR_KIND_WINDOW_FRAME_ROWS: case EXPR_KIND_WINDOW_FRAME_GROUPS: diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out index c4f7b187f5baf..005f1268f4b2c 100644 --- a/src/test/regress/expected/tsrf.out +++ b/src/test/regress/expected/tsrf.out @@ -267,7 +267,21 @@ ERROR: window function calls cannot contain set-returning function calls LINE 1: SELECT min(generate_series(1, 3)) OVER() FROM few; ^ HINT: You might be able to move the set-returning function into a LATERAL FROM item. --- SRFs are normally computed after window functions +--- ... nor in window definitions +SELECT sum(id) OVER (PARTITION BY generate_series(1, 3)) FROM few; +ERROR: set-returning functions are not allowed in window definitions +LINE 1: SELECT sum(id) OVER (PARTITION BY generate_series(1, 3)) FRO... + ^ +SELECT sum(id) OVER (ORDER BY generate_series(1, 3)) FROM few; +ERROR: set-returning functions are not allowed in window definitions +LINE 1: SELECT sum(id) OVER (ORDER BY generate_series(1, 3)) FROM fe... + ^ +SELECT sum(id) OVER (ROWS BETWEEN UNBOUNDED PRECEDING + AND generate_series(1, 3) FOLLOWING) FROM few; +ERROR: set-returning functions are not allowed in window definitions +LINE 2: AND generate_series(1, 3) FOLLOWING) FR... + ^ +-- SRFs are computed after window functions SELECT id,lag(id) OVER(), count(*) OVER(), generate_series(1,3) FROM few; id | lag | count | generate_series ----+-----+-------+----------------- @@ -282,15 +296,6 @@ SELECT id,lag(id) OVER(), count(*) OVER(), generate_series(1,3) FROM few; 3 | 2 | 3 | 3 (9 rows) --- unless referencing SRFs -SELECT SUM(count(*)) OVER(PARTITION BY generate_series(1,3) ORDER BY generate_series(1,3)), generate_series(1,3) g FROM few GROUP BY g; - sum | g ------+--- - 3 | 1 - 3 | 2 - 3 | 3 -(3 rows) - -- sorting + grouping SELECT few.dataa, count(*), min(id), max(id), generate_series(1,3) FROM few GROUP BY few.dataa ORDER BY 5, 1; dataa | count | min | max | generate_series diff --git a/src/test/regress/sql/tsrf.sql b/src/test/regress/sql/tsrf.sql index 7c22529a0db0a..8442ba9e743b3 100644 --- a/src/test/regress/sql/tsrf.sql +++ b/src/test/regress/sql/tsrf.sql @@ -82,10 +82,14 @@ SELECT sum((3 = ANY(SELECT lag(x) over(order by x) -- SRFs are not allowed in window function arguments, either SELECT min(generate_series(1, 3)) OVER() FROM few; --- SRFs are normally computed after window functions +--- ... nor in window definitions +SELECT sum(id) OVER (PARTITION BY generate_series(1, 3)) FROM few; +SELECT sum(id) OVER (ORDER BY generate_series(1, 3)) FROM few; +SELECT sum(id) OVER (ROWS BETWEEN UNBOUNDED PRECEDING + AND generate_series(1, 3) FOLLOWING) FROM few; + +-- SRFs are computed after window functions SELECT id,lag(id) OVER(), count(*) OVER(), generate_series(1,3) FROM few; --- unless referencing SRFs -SELECT SUM(count(*)) OVER(PARTITION BY generate_series(1,3) ORDER BY generate_series(1,3)), generate_series(1,3) g FROM few GROUP BY g; -- sorting + grouping SELECT few.dataa, count(*), min(id), max(id), generate_series(1,3) FROM few GROUP BY few.dataa ORDER BY 5, 1; From d8312d954637b69e51729266c614df522171a876 Mon Sep 17 00:00:00 2001 From: Lukas Fittl Date: Fri, 26 Jun 2026 21:35:15 -0700 Subject: [PATCH 36/36] Use fast-hash for Query ID jumbling This replaces the use of the 1024 byte jumble buffer and use of hash_any_extended (Jenkins-style hash function) for query id jumbling with the newer fasthash_ API that avoids the use of a separate buffer, and carries less performance overhead. Both hash function are non-cryptographic hashes that optimize for performance, but fasthash is the more modern function and passes the SMhasher test suite for hash collisions. Author: Lukas Fittl Reviewed-by: Discussion: --- src/backend/nodes/queryjumblefuncs.c | 106 +++++++++++++-------------- src/include/nodes/queryjumble.h | 14 ++-- 2 files changed, 56 insertions(+), 64 deletions(-) diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c index 7c63766a51c5d..9e5b241e6ff4e 100644 --- a/src/backend/nodes/queryjumblefuncs.c +++ b/src/backend/nodes/queryjumblefuncs.c @@ -39,7 +39,7 @@ #include "access/transam.h" #include "catalog/pg_proc.h" -#include "common/hashfn.h" +#include "common/hashfn_unstable.h" #include "common/int.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -48,8 +48,6 @@ #include "parser/scanner.h" #include "parser/scansup.h" -#define JUMBLE_SIZE 1024 /* query serialization buffer size */ - /* GUC parameters */ int compute_query_id = COMPUTE_QUERY_ID_AUTO; @@ -186,8 +184,7 @@ InitJumble(void) jstate = palloc_object(JumbleState); /* Set up workspace for query jumbling */ - jstate->jumble = (unsigned char *) palloc(JUMBLE_SIZE); - jstate->jumble_len = 0; + fasthash_init(&jstate->jumble, 0); jstate->clocations_buf_size = 32; jstate->clocations = (LocationLen *) palloc(jstate->clocations_buf_size * sizeof(LocationLen)); @@ -221,14 +218,18 @@ DoJumble(JumbleState *jstate, Node *node) if (jstate->has_squashed_lists) jstate->highest_extern_param_id = 0; - /* Process the jumble buffer and produce the hash value */ - return DatumGetInt64(hash_any_extended(jstate->jumble, - jstate->jumble_len, - 0)); + /* + * Produce the final hash; we pass zero as the tweak since we already + * emitted the length following each variable-length value + */ + return (int64) fasthash_final64(&jstate->jumble, 0); } /* - * AppendJumbleInternal: Internal function for appending to the jumble buffer + * AppendJumbleInternal: Internal function for appending to the jumble state + * + * Accumulates the given value into the streaming fasthash state, processing + * it in 8-byte chunks. * * Note: Callers must ensure that size > 0. */ @@ -236,59 +237,24 @@ static pg_attribute_always_inline void AppendJumbleInternal(JumbleState *jstate, const unsigned char *item, Size size) { - unsigned char *jumble = jstate->jumble; - Size jumble_len = jstate->jumble_len; + Size orig_size PG_USED_FOR_ASSERTS_ONLY = size; /* Ensure the caller didn't mess up */ Assert(size > 0); - /* - * Fast path for when there's enough space left in the buffer. This is - * worthwhile as means the memcpy can be inlined into very efficient code - * when 'size' is a compile-time constant. - */ - if (likely(size <= JUMBLE_SIZE - jumble_len)) + while (size >= FH_SIZEOF_ACCUM) { - memcpy(jumble + jumble_len, item, size); - jstate->jumble_len += size; - -#ifdef USE_ASSERT_CHECKING - jstate->total_jumble_len += size; -#endif - - return; + fasthash_accum(&jstate->jumble, (const char *) item, FH_SIZEOF_ACCUM); + item += FH_SIZEOF_ACCUM; + size -= FH_SIZEOF_ACCUM; } - /* - * Whenever the jumble buffer is full, we hash the current contents and - * reset the buffer to contain just that hash value, thus relying on the - * hash to summarize everything so far. - */ - do - { - Size part_size; - - if (unlikely(jumble_len >= JUMBLE_SIZE)) - { - int64 start_hash; - - start_hash = DatumGetInt64(hash_any_extended(jumble, - JUMBLE_SIZE, 0)); - memcpy(jumble, &start_hash, sizeof(start_hash)); - jumble_len = sizeof(start_hash); - } - part_size = Min(size, JUMBLE_SIZE - jumble_len); - memcpy(jumble + jumble_len, item, part_size); - jumble_len += part_size; - item += part_size; - size -= part_size; + if (size > 0) + fasthash_accum(&jstate->jumble, (const char *) item, size); #ifdef USE_ASSERT_CHECKING - jstate->total_jumble_len += part_size; + jstate->total_jumble_len += orig_size; #endif - } while (size > 0); - - jstate->jumble_len = jumble_len; } /* @@ -302,6 +268,12 @@ AppendJumble(JumbleState *jstate, const unsigned char *value, Size size) FlushPendingNulls(jstate); AppendJumbleInternal(jstate, value, size); + + /* + * Fold in the length so variable-length values are self-delimiting, which + * lets DoJumble finalize with a zero tweak (see hashfn_unstable.h). + */ + fasthash_accum(&jstate->jumble, (const char *) &size, sizeof(size)); } /* @@ -369,9 +341,31 @@ AppendJumble64(JumbleState *jstate, const unsigned char *value) AppendJumbleInternal(jstate, value, 8); } +/* + * AppendJumbleString + * Add the given NUL-terminated string to the jumble state. + * + * Length is computed on the fly while hashing, avoiding a separate strlen. + */ +static pg_noinline void +AppendJumbleString(JumbleState *jstate, const char *str) +{ + Size size; + + if (jstate->pending_nulls > 0) + FlushPendingNulls(jstate); + + size = fasthash_accum_cstring(&jstate->jumble, str); + fasthash_accum(&jstate->jumble, (const char *) &size, sizeof(size)); + +#ifdef USE_ASSERT_CHECKING + jstate->total_jumble_len += size; +#endif +} + /* * FlushPendingNulls - * Incorporate the pending_nulls value into the jumble buffer. + * Incorporate the pending_nulls value into the jumble hash. * * Note: Callers must ensure that there's at least 1 pending NULL. */ @@ -549,7 +543,7 @@ do { \ #define JUMBLE_STRING(str) \ do { \ if (expr->str) \ - AppendJumble(jstate, (const unsigned char *) (expr->str), strlen(expr->str) + 1); \ + AppendJumbleString(jstate, expr->str); \ else \ AppendJumbleNull(jstate); \ } while(0) @@ -600,7 +594,7 @@ _jumbleNode(JumbleState *jstate, Node *node) break; } - /* Ensure we added something to the jumble buffer */ + /* Ensure we added something to the jumble */ Assert(jstate->total_jumble_len > prev_jumble_len); } diff --git a/src/include/nodes/queryjumble.h b/src/include/nodes/queryjumble.h index f331449ba78f6..253911c0165d5 100644 --- a/src/include/nodes/queryjumble.h +++ b/src/include/nodes/queryjumble.h @@ -14,6 +14,7 @@ #ifndef QUERYJUMBLE_H #define QUERYJUMBLE_H +#include "common/hashfn_unstable.h" #include "nodes/parsenodes.h" /* @@ -37,11 +38,8 @@ typedef struct LocationLen */ typedef struct JumbleState { - /* Jumble of current query tree */ - unsigned char *jumble; - - /* Number of bytes used in jumble[] */ - Size jumble_len; + /* Streaming hash state for the current query jumble */ + fasthash_state jumble; /* Array of locations of constants that should be removed */ LocationLen *clocations; @@ -66,13 +64,13 @@ typedef struct JumbleState /* * Count of the number of NULL nodes seen since last appending a value. - * These are flushed out to the jumble buffer before subsequent appends - * and before performing the final jumble hash. + * These are flushed out to the jumble hash before subsequent appends and + * before performing the final jumble hash. */ unsigned int pending_nulls; #ifdef USE_ASSERT_CHECKING - /* The total number of bytes added to the jumble buffer */ + /* The total number of bytes added to the jumble */ Size total_jumble_len; #endif } JumbleState;