From d9cd9b4d7e147fc4965195c765f191ac69593c43 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 13 May 2026 11:44:31 +0900 Subject: [PATCH 001/250] Fix stale COPY progress during logical replication table sync Previously, pg_stat_progress_copy in the subscriber could continue to show the initial COPY operation for logical replication table synchronization as active even after the data copy had finished. The stale progress entry remained visible until synchronization caught up with the publisher. This happened because the table synchronization code called BeginCopyFrom() and CopyFrom(), but failed to call EndCopyFrom() afterward. This commit fixes the issue by adding the missing EndCopyFrom() call so that the COPY progress state in the subscriber is cleared as soon as the initial data copy completes. Backpatch to all supported branches. Author: Shinya Kato Reviewed-by: Fujii Masao Reviewed-by: ChangAo Chen Reviewed-by: Chao Li Discussion: https://postgr.es/m/CAOzEurQKuy3RiPkd=25PEwEzaqHuGvEOf=X7vaVzhgNjaukYzA@mail.gmail.com Backpatch-through: 14 --- src/backend/replication/logical/tablesync.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c index 49d4ec3bee4..92e4e383d77 100644 --- a/src/backend/replication/logical/tablesync.c +++ b/src/backend/replication/logical/tablesync.c @@ -1277,6 +1277,7 @@ copy_table(Relation rel) /* Do the copy */ (void) CopyFrom(cstate); + EndCopyFrom(cstate); logicalrep_rel_close(relmapentry, NoLock); } From 89192080f0a9599d121a8ba85a94b41b8c424cf5 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 13 May 2026 14:43:46 +0900 Subject: [PATCH 002/250] Add more tests for corrupted data with pglz_decompress() Two cases fixed by 2b5ba2a0a141 were not covered, to emulate the handling of corrupted data, for: - set control bit with a valid 2-byte match tag where offset is 0. - set control bit with a valid 2-byte match tag where offset exceeds output written. Oversight in 67d318e70402. Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/agF4xkIdRcrCIprs@paquier.xyz Backpatch-through: 14 --- src/test/regress/expected/compression_pglz.out | 12 ++++++++++++ src/test/regress/sql/compression_pglz.sql | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/test/regress/expected/compression_pglz.out b/src/test/regress/expected/compression_pglz.out index 0ef49d42506..066a3317c65 100644 --- a/src/test/regress/expected/compression_pglz.out +++ b/src/test/regress/expected/compression_pglz.out @@ -60,6 +60,18 @@ SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); ERROR: pglz_decompress failed SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); ERROR: pglz_decompress failed +-- Corrupted compressed data. Set control bit with a valid 2-byte match +-- tag where offset exceeds output written. +SELECT test_pglz_decompress('\x011001'::bytea, 1024, false); +ERROR: pglz_decompress failed +SELECT test_pglz_decompress('\x011001'::bytea, 1024, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. Set control bit with a valid 2-byte match +-- tag where offset is 0. +SELECT test_pglz_decompress('\x010300'::bytea, 1024, false); +ERROR: pglz_decompress failed +SELECT test_pglz_decompress('\x010300'::bytea, 1024, true); +ERROR: pglz_decompress failed -- Clean up DROP FUNCTION test_pglz_compress; DROP FUNCTION test_pglz_decompress; diff --git a/src/test/regress/sql/compression_pglz.sql b/src/test/regress/sql/compression_pglz.sql index a44af02afb7..dbd37f7d4eb 100644 --- a/src/test/regress/sql/compression_pglz.sql +++ b/src/test/regress/sql/compression_pglz.sql @@ -48,6 +48,16 @@ SELECT test_pglz_decompress('\x01ff'::bytea, 1024, true); SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); +-- Corrupted compressed data. Set control bit with a valid 2-byte match +-- tag where offset exceeds output written. +SELECT test_pglz_decompress('\x011001'::bytea, 1024, false); +SELECT test_pglz_decompress('\x011001'::bytea, 1024, true); + +-- Corrupted compressed data. Set control bit with a valid 2-byte match +-- tag where offset is 0. +SELECT test_pglz_decompress('\x010300'::bytea, 1024, false); +SELECT test_pglz_decompress('\x010300'::bytea, 1024, true); + -- Clean up DROP FUNCTION test_pglz_compress; DROP FUNCTION test_pglz_decompress; From 98dd6c2046965e51da015681e81c20109be46d71 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 14 May 2026 12:30:34 +0900 Subject: [PATCH 003/250] pgbench: fix verbose error message corruption with multiple threads When pgbench runs with multiple threads and verbose error reporting is enabled (--verbose-errors), multiple clients can build verbose error messages concurrently. Previously, a function-local static PQExpBuffer was used for these messages, causing the buffer to be shared across threads. This was not thread-safe and could result in corrupted or incorrect log output. Fix this by using a local PQExpBufferData instead of a static buffer. This keeps verbose error messages correct during concurrent execution. Backpatch to v15, where this issue was introduced. Author: Fujii Masao Reviewed-by: Michael Paquier Reviewed-by: Alex Guo Reviewed-by: Chao Li Discussion: https://postgr.es/m/CAHGQGwER1AjGXpkKB9t9820NBhMQ_Ghv7=HsKeodUr3=SZsF4g@mail.gmail.com Backpatch-through: 15 --- src/bin/pgbench/pgbench.c | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c index f4dd8c0b474..7913dde6ceb 100644 --- a/src/bin/pgbench/pgbench.c +++ b/src/bin/pgbench/pgbench.c @@ -3623,22 +3623,19 @@ getTransactionStatus(PGconn *con) static void printVerboseErrorMessages(CState *st, pg_time_usec_t *now, bool is_retry) { - static PQExpBuffer buf = NULL; + PQExpBufferData buf; - if (buf == NULL) - buf = createPQExpBuffer(); - else - resetPQExpBuffer(buf); + initPQExpBuffer(&buf); - printfPQExpBuffer(buf, "client %d ", st->id); - appendPQExpBufferStr(buf, (is_retry ? - "repeats the transaction after the error" : - "ends the failed transaction")); - appendPQExpBuffer(buf, " (try %u", st->tries); + printfPQExpBuffer(&buf, "client %d ", st->id); + appendPQExpBufferStr(&buf, (is_retry ? + "repeats the transaction after the error" : + "ends the failed transaction")); + appendPQExpBuffer(&buf, " (try %u", st->tries); /* Print max_tries if it is not unlimited. */ if (max_tries) - appendPQExpBuffer(buf, "/%u", max_tries); + appendPQExpBuffer(&buf, "/%u", max_tries); /* * If the latency limit is used, print a percentage of the current @@ -3647,12 +3644,14 @@ printVerboseErrorMessages(CState *st, pg_time_usec_t *now, bool is_retry) if (latency_limit) { pg_time_now_lazy(now); - appendPQExpBuffer(buf, ", %.3f%% of the maximum time of tries was used", + appendPQExpBuffer(&buf, ", %.3f%% of the maximum time of tries was used", (100.0 * (*now - st->txn_scheduled) / latency_limit)); } - appendPQExpBufferStr(buf, ")\n"); + appendPQExpBufferStr(&buf, ")\n"); - pg_log_info("%s", buf->data); + pg_log_info("%s", buf.data); + + termPQExpBuffer(&buf); } /* From 1cd37a7a8dc6bbd3127f4df6dddf1ae79b60f81e Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 14 May 2026 12:21:03 +0300 Subject: [PATCH 004/250] Add tests for cross-session temp table access Add a TAP test in src/test/modules/test_misc that documents what happens when one session attempts to read or modify another session's temporary table. This commit only adds tests; it does not change backend behavior, so the assertions reflect current behavior: - SELECT, UPDATE, DELETE, MERGE, COPY on a table without an index silently succeed with no error and zero rows / zero affected rows. These commands run through the read-stream path, which currently bypasses the RELATION_IS_OTHER_TEMP() check. This is the underlying bug to be fixed in a follow-up. - INSERT errors with "cannot access temporary tables of other sessions" because hio.c calls ReadBufferExtended() to find a page with free space and is caught by the existing check there. - Index scan errors via the same existing check, reached through nbtree -> ReadBuffer -> ReadBufferExtended. - TRUNCATE / ALTER TABLE / ALTER INDEX / CLUSTER fail with their command-specific error messages. - VACUUM is silently skipped to avoid noise during database-wide VACUUM (vacuum_rel() returns without warning). - DROP TABLE is intentionally allowed: DROP does not touch the table's contents, and autovacuum relies on this to clean up temp relations orphaned by a crashed backend. - ALTER FUNCTION / DROP FUNCTION on an owner-created function over its own temp row type work as catalog operations -- they don't read the underlying data. - CREATE FUNCTION from a separate session, using another session's temp row type as an argument, is allowed but emits a NOTICE: the function is moved into the creator's pg_temp namespace with an auto-dependency on the borrowed type, so it disappears together with the session that created it. - A bare DROP TABLE on a temp table that has a cross-session dependent function fails with a catalog-level dependency error. - LOCK TABLE in ACCESS SHARE mode on another session's temp table succeeds and properly blocks the owner's session-exit cleanup (which acquires AccessExclusiveLock via findDependentObjects). This exercises the same LockRelationOid path used by autovacuum when cleaning up orphaned temp relations. - When the owner session ends, the normal session-exit cleanup cascades through DEPENDENCY_NORMAL and removes both the temp objects and any cross-session functions that depended on them. Also, document the contract for RELATION_IS_OTHER_TEMP() so that future buffer-access entry points enforce the same rule. Backpatch this through PostgreSQL 17, where b7b0f3f27241 introduces a code path bypassing this check. Author: Jim Jones Author: Daniil Davydov <3danissimo@gmail.com> Co-authored-by: Alexander Korotkov Reviewed-by: Michael Paquier Reviewed-by: Soumya S Murali Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAJDiXghdFcZ8%3Dnh4G69te7iRr3Q0uFyXxb3ZdG09_GTNZXwH0g%40mail.gmail.com Backpatch-through: 17 --- src/include/utils/rel.h | 14 + src/test/modules/test_misc/meson.build | 1 + .../test_misc/t/013_temp_obj_multisession.pl | 260 ++++++++++++++++++ 3 files changed, 275 insertions(+) create mode 100644 src/test/modules/test_misc/t/013_temp_obj_multisession.pl diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index b552359915f..b10f909f227 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -664,6 +664,20 @@ RelationCloseSmgr(Relation relation) * RELATION_IS_OTHER_TEMP * Test for a temporary relation that belongs to some other session. * + * Reading another session's temp-table data through never works right: + * the owning session keeps the data in its private local buffer pool, + * which we cannot access. The macro is therefore used at the buffer-manager + * level to reject such accesses, and by command-level code (TRUNCATE, + * ALTER TABLE, VACUUM, CLUSTER, REINDEX, ...) for command-specific error + * messages. + * + * Currenlty buffer manager checks include only ReadBufferExtended(), and + * PrefetchBuffer(); while ReadBuffer_common(), read_stream_begin_impl(), and + * StartReadBuffersImpl() are not covered. As a result, read paths that + * bypass ReadBufferExtended() -- notably sequential scans that go through + * the read-stream API -- silently return no rows when targeted at another + * session's temp table instead of failing. + * * Beware of multiple eval of argument */ #define RELATION_IS_OTHER_TEMP(relation) \ diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build index 6b1e730bf46..216c38016ae 100644 --- a/src/test/modules/test_misc/meson.build +++ b/src/test/modules/test_misc/meson.build @@ -17,6 +17,7 @@ tests += { 't/006_signal_autovacuum.pl', 't/007_catcache_inval.pl', 't/008_replslot_single_user.pl', + 't/013_temp_obj_multisession.pl', ], }, } diff --git a/src/test/modules/test_misc/t/013_temp_obj_multisession.pl b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl new file mode 100644 index 00000000000..80b67c5e85e --- /dev/null +++ b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl @@ -0,0 +1,260 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Tests that one session cannot read or modify data in another session's +# temporary table. Each session keeps its temp data in its own local +# buffer pool, and a different backend has no visibility into those +# buffers, so any command that needs to look at the data must be +# rejected. +# +# DROP TABLE is intentionally allowed: it does not touch the table's +# contents, and autovacuum relies on this to clean up orphaned temp +# relations left behind by a crashed backend. +# +# A regression caught here typically means a new buffer-access entry +# point bypasses the RELATION_IS_OTHER_TEMP() check. See +# ReadBuffer_common(), StartReadBuffersImpl(), and read_stream_begin_impl() +# for the existing checks. When adding a new command or buffer-access +# path, also add a corresponding case below. + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::BackgroundPsql; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('temp_lock'); +$node->init; +$node->append_conf('postgresql.conf', 'log_lock_waits=on'); +$node->start; + +# Owner session. Created via background_psql so it stays alive while +# the second session probes its temp objects. +my $psql1 = $node->background_psql('postgres'); + +# Initially create the table without an index, so read paths go straight +# through the read-stream / buffer-manager entry points without being +# masked by an index scan that would hit ReadBuffer_common from nbtree. +$psql1->query_safe(q(CREATE TEMP TABLE foo AS SELECT 42 AS val;)); + +# Resolve the owner's temp schema so the probing session can refer to +# the table by a fully-qualified name. +my $tempschema = $node->safe_psql( + 'postgres', + q{ + SELECT n.nspname + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE relname = 'foo' AND relpersistence = 't'; + } +); +chomp $tempschema; +ok($tempschema =~ /^pg_temp_\d+$/, "got temp schema: $tempschema"); + +my ($stdout, $stderr); + +# DML and SELECT have to read the table's data and therefore go through +# the buffer manager. With no index on the table, the planner cannot +# use index access, so SELECT/UPDATE/DELETE/MERGE/COPY all run through +# the read-stream path. +# +# XXX: in current code, the read-stream path bypasses the +# RELATION_IS_OTHER_TEMP() check, so these commands silently see no +# rows / report zero affected rows -- the visible symptom of the bug +# this test suite documents. A follow-up patch will route the check +# through read_stream_begin_impl() and these assertions will be +# updated to expect "cannot access temporary tables of other sessions". + +$node->psql( + 'postgres', + "SELECT val FROM $tempschema.foo;", + stdout => \$stdout, + stderr => \$stderr); +is($stderr, '', 'SELECT (currently no error -- bug to be fixed)'); + +# INSERT goes through hio.c which calls ReadBufferExtended() to find a +# page with free space; that hits the existing check before any data is +# written. This case currently errors as expected. +$node->psql( + 'postgres', + "INSERT INTO $tempschema.foo VALUES (73);", + stderr => \$stderr); +like( + $stderr, + qr/cannot access temporary tables of other sessions/, + 'INSERT (caught via hio.c)'); + +$node->psql( + 'postgres', + "UPDATE $tempschema.foo SET val = NULL;", + stderr => \$stderr); +is($stderr, '', 'UPDATE (currently no error -- bug to be fixed)'); + +$node->psql('postgres', "DELETE FROM $tempschema.foo;", stderr => \$stderr); +is($stderr, '', 'DELETE (currently no error -- bug to be fixed)'); + +$node->psql( + 'postgres', + "MERGE INTO $tempschema.foo USING (VALUES (42)) AS s(val) " + . "ON foo.val = s.val WHEN MATCHED THEN DELETE;", + stderr => \$stderr); +is($stderr, '', 'MERGE (currently no error -- bug to be fixed)'); + +$node->psql('postgres', "COPY $tempschema.foo TO STDOUT;", + stderr => \$stderr); +is($stderr, '', 'COPY (currently no error -- bug to be fixed)'); + +# DDL and maintenance commands have their own command-specific checks +# (older than the buffer-manager check above), so they fail with +# command-specific error messages. Verifying them here documents the +# expected behaviour and guards against accidental removal of those +# checks. + +$node->psql('postgres', "TRUNCATE TABLE $tempschema.foo;", + stderr => \$stderr); +like($stderr, qr/cannot truncate temporary tables of other sessions/, + 'TRUNCATE'); + +$node->psql( + 'postgres', + "ALTER TABLE $tempschema.foo ALTER COLUMN val TYPE bigint;", + stderr => \$stderr); +like($stderr, qr/cannot alter temporary tables of other sessions/, + 'ALTER TABLE'); + +# VACUUM silently skips other sessions' temp tables (vacuum_rel() returns +# without warning to avoid noise during database-wide VACUUM). Verify +# that no error is reported, and that no buffer-access path is hit. +$node->psql('postgres', "VACUUM $tempschema.foo;", stderr => \$stderr); +is($stderr, '', 'VACUUM is silently skipped'); + +$node->psql('postgres', "CLUSTER $tempschema.foo;", stderr => \$stderr); +like($stderr, qr/cannot cluster temporary tables of other sessions/, + 'CLUSTER'); + +# Now create an index to exercise the index-scan path. nbtree calls +# ReadBuffer (which is ReadBufferExtended -> ReadBuffer_common), so +# this exercises a different chain of buffer-manager entry points. +$psql1->query_safe(q(CREATE INDEX ON foo(val);)); + +$node->psql( + 'postgres', + "SET enable_seqscan = off; SELECT val FROM $tempschema.foo WHERE val = 42;", + stderr => \$stderr); +like( + $stderr, + qr/cannot access temporary tables of other sessions/, + 'index scan (ReadBuffer_common via nbtree)'); + +# ALTER INDEX goes through the same CheckAlterTableIsSafe() path as +# ALTER TABLE, so it produces the same error. +$node->psql( + 'postgres', + "ALTER INDEX $tempschema.foo_val_idx SET (fillfactor = 50);", + stderr => \$stderr); +like($stderr, qr/cannot alter temporary tables of other sessions/, + 'ALTER INDEX'); + +# A function created by the owner in its own pg_temp using its own +# row type can be observed via the catalog by a separate session. +# ALTER FUNCTION and DROP FUNCTION on it must work as catalog +# operations -- they don't read the underlying table -- which +# documents the boundary between catalog and data access for temp +# objects. +$psql1->query_safe( + q[CREATE FUNCTION pg_temp.foo_id(r foo) RETURNS int LANGUAGE SQL ] + . q[AS 'SELECT r.val';]); + +$node->psql( + 'postgres', + "ALTER FUNCTION $tempschema.foo_id($tempschema.foo) " + . "SET search_path = pg_catalog;", + stderr => \$stderr); +is($stderr, '', 'ALTER FUNCTION on function over other session\'s row type'); + +$node->psql( + 'postgres', + "DROP FUNCTION $tempschema.foo_id($tempschema.foo);", + stderr => \$stderr); +is($stderr, '', 'DROP FUNCTION on function over other session\'s row type'); + +# DROP TABLE on another session's temp table is intentionally permitted. +# DROP doesn't touch the table's contents, and autovacuum relies on this +# to remove temp relations orphaned by a crashed backend. Verify that +# the bare DROP succeeds without error. +$node->psql('postgres', "DROP TABLE $tempschema.foo;", stderr => \$stderr); +is($stderr, '', 'DROP TABLE is allowed'); + +# Cross-session CREATE FUNCTION scenario. The owner creates a fresh +# temp table foo2 in its pg_temp namespace, and a separate session +# then creates a function whose argument type is that row type. +# PostgreSQL allows this and emits a NOTICE: the function is moved +# into the creator's pg_temp namespace with an auto-dependency on +# the borrowed type, so it disappears together with the session that +# created it. +$psql1->query_safe(q(CREATE TEMP TABLE foo2 AS SELECT 42 AS val;)); + +$node->safe_psql('postgres', + "CREATE FUNCTION public.cross_session_func(r $tempschema.foo2) " + . "RETURNS int LANGUAGE SQL AS 'SELECT 1';"); + +# A bare DROP TABLE on foo2 now fails because cross_session_func +# depends on its row type. This is normal SQL dependency behaviour +# and documents that DROP itself is not blocked by buffer-manager +# checks -- we get a catalog-level error instead. +$node->psql('postgres', "DROP TABLE $tempschema.foo2;", stderr => \$stderr); +like( + $stderr, + qr/cannot drop table .*\.foo2 because other objects depend on it/, + 'DROP TABLE blocked by cross-session dependency'); + +my $foo2_oid = $node->safe_psql('postgres', + "SELECT oid FROM pg_class WHERE relname='foo2';"); + +# Cross-session LOCK TABLE scenario. Ensure that LockRelationOid is working +# properly for other temp tables since this mechanism is also used by +# autovacuum during orphaned tables cleanup. +my $psql2 = $node->background_psql('postgres'); +$psql2->query_safe( + qq{ + BEGIN; + LOCK TABLE $tempschema.foo2 IN ACCESS SHARE MODE; +}); + +# When the owner session ends, its temp objects are dropped via the +# normal session-exit cleanup, which cascades through +# DEPENDENCY_NORMAL and also removes the cross-session function that +# depended on the temp row type. This is the same mechanism +# autovacuum relies on to clean up temp relations left behind by a +# crashed backend. +# Access share lock on the foo2 will block session-exit cleanup, because an +# owner will try to acquire deletion lock all its temp objects via +# findDependentObjects. +my $log_offset = -s $node->logfile; +$psql1->quit; + +# Check whether session-exit cleanup is blocked. +$node->wait_for_log(qr/waiting for AccessExclusiveLock on relation $foo2_oid/, + $log_offset); + +# Release lock on foo2 and allow session-exit cleanup to finish. +$psql2->query_safe(q(COMMIT;)); +$psql2->quit; + +# After releasing the lock, the owner can finally acquire +# AccessExclusiveLock on foo2 and finish session-exit cleanup. Verify +# directly that both foo2 (the locked temp table) and cross_session_func +# (which depended on its row type) have been dropped. Both being gone +# confirms the owner's cleanup got past the blocked findDependentObjects() +# call and completed normally. +$node->poll_query_until('postgres', + "SELECT NOT EXISTS (SELECT 1 FROM pg_class WHERE oid = $foo2_oid)") + or die "foo2 was not cleaned up after owner session exit"; + +is( $node->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_proc WHERE proname = 'cross_session_func'"), + '0', + 'cross_session_func cleaned up by cascade from foo2'); + +done_testing(); From 1b0dd08157bf945909849c5e73d9e3f5b057c63b Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 14 May 2026 12:25:19 +0300 Subject: [PATCH 005/250] Prevent access to other sessions' temp tables Commit b7b0f3f2724 ("Use streaming I/O in sequential scans") routed sequential scans through read_stream_next_buffer(), bypassing the RELATION_IS_OTHER_TEMP() check in ReadBufferExtended(). As a result, a superuser can attempt to read or modify temp tables of other sessions through the read-stream path. When the query plan uses no index, SELECT/UPDATE/DELETE/MERGE silently see no rows / report zero affected rows, and COPY produces an empty output -- because the buffer manager has no visibility into the owning session's local buffers and silently returns nothing. Any query plan that uses, for instance, a btree index still errors out via the existing check in ReadBufferExtended(), which is reached from hio.c and nbtree respectively, but this is incidental. Fix by enforcing RELATION_IS_OTHER_TEMP() at the three additional buffer-manager entry points: - read_stream_begin_impl() rejects the read at stream setup time, covering sequential and bitmap scans that go through the read-stream path. - ReadBuffer_common() becomes the canonical place for the check, consolidating the existing one previously kept in ReadBufferExtended(). All ReadBufferExtended() callers go through ReadBuffer_common(), so the consolidation is behavior-preserving. - StartReadBuffersImpl() catches direct callers of StartReadBuffers() that bypass both of the above. This is currently defense-in-depth, but documents the contract for future code. The companion test in src/test/modules/test_misc was added in the preceding commit; this commit updates the assertions for SELECT, UPDATE, DELETE, MERGE, and COPY (which previously documented the bug as silent success) to expect the new error. Author: Jim Jones Author: Daniil Davydov <3danissimo@gmail.com> Co-authored-by: Alexander Korotkov Reviewed-by: Michael Paquier Reviewed-by: Soumya S Murali Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAJDiXghdFcZ8%3Dnh4G69te7iRr3Q0uFyXxb3ZdG09_GTNZXwH0g%40mail.gmail.com Backpatch-through: 17 --- src/backend/storage/aio/read_stream.c | 10 ++++++ src/backend/storage/buffer/bufmgr.c | 33 ++++++++++++------- src/include/utils/rel.h | 17 ++++------ .../test_misc/t/013_temp_obj_multisession.pl | 27 +++++++-------- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.c index 6ab2e5a53f4..17e3a68a822 100644 --- a/src/backend/storage/aio/read_stream.c +++ b/src/backend/storage/aio/read_stream.c @@ -555,6 +555,16 @@ read_stream_begin_impl(int flags, uint32 max_possible_buffer_limit; Oid tablespace_id; + /* + * Reject attempts to read non-local temporary relations; we would be + * likely to get wrong data since we have no visibility into the owning + * session's local buffers. + */ + if (rel && RELATION_IS_OTHER_TEMP(rel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot access temporary tables of other sessions"))); + /* * Decide how many I/Os we will allow to run at the same time. This * number also affects how far we look ahead for opportunities to start diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 0212cab1026..27fd7e9720a 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -655,7 +655,7 @@ PrefetchBuffer(Relation reln, ForkNumber forkNum, BlockNumber blockNum) if (RelationUsesLocalBuffers(reln)) { - /* see comments in ReadBufferExtended */ + /* see comments in ReadBuffer_common */ if (RELATION_IS_OTHER_TEMP(reln)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -807,19 +807,10 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum, { Buffer buf; - /* - * Reject attempts to read non-local temporary relations; we would be - * likely to get wrong data since we have no visibility into the owning - * session's local buffers. - */ - if (RELATION_IS_OTHER_TEMP(reln)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot access temporary tables of other sessions"))); - /* * Read the buffer, and update pgstat counters to reflect a cache hit or - * miss. + * miss. The other-session temp-relation check is enforced by + * ReadBuffer_common(). */ buf = ReadBuffer_common(reln, RelationGetSmgr(reln), 0, forkNum, blockNum, mode, strategy); @@ -1200,6 +1191,18 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence, int flags; char persistence; + /* + * Reject attempts to read non-local temporary relations; we would be + * likely to get wrong data since we have no visibility into the owning + * session's local buffers. This is the canonical place for the check, + * covering the ReadBufferExtended() entry point and any other caller that + * supplies a Relation. + */ + if (rel && RELATION_IS_OTHER_TEMP(rel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot access temporary tables of other sessions"))); + /* * Backward compatibility path, most code should use ExtendBufferedRel() * instead, as acquiring the extension lock inside ExtendBufferedRel() @@ -1274,6 +1277,12 @@ StartReadBuffersImpl(ReadBuffersOperation *operation, Assert(*nblocks > 0); Assert(*nblocks <= MAX_IO_COMBINE_LIMIT); + /* see comments in ReadBuffer_common */ + if (operation->rel && RELATION_IS_OTHER_TEMP(operation->rel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot access temporary tables of other sessions"))); + for (int i = 0; i < actual_nblocks; ++i) { bool found; diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index b10f909f227..2f86c79a907 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -666,17 +666,12 @@ RelationCloseSmgr(Relation relation) * * Reading another session's temp-table data through never works right: * the owning session keeps the data in its private local buffer pool, - * which we cannot access. The macro is therefore used at the buffer-manager - * level to reject such accesses, and by command-level code (TRUNCATE, - * ALTER TABLE, VACUUM, CLUSTER, REINDEX, ...) for command-specific error - * messages. - * - * Currenlty buffer manager checks include only ReadBufferExtended(), and - * PrefetchBuffer(); while ReadBuffer_common(), read_stream_begin_impl(), and - * StartReadBuffersImpl() are not covered. As a result, read paths that - * bypass ReadBufferExtended() -- notably sequential scans that go through - * the read-stream API -- silently return no rows when targeted at another - * session's temp table instead of failing. + * which we cannot access. Existing buffer-manager entry points + * (ReadBuffer_common(), StartReadBuffersImpl(), read_stream_begin_impl(), + * and PrefetchBuffer()) already enforce this; any new buffer-access entry + * points must do the same. Command-level code (TRUNCATE, ALTER TABLE, + * VACUUM, CLUSTER, REINDEX, ...) additionally uses this macro for + * command-specific error messages. * * Beware of multiple eval of argument */ diff --git a/src/test/modules/test_misc/t/013_temp_obj_multisession.pl b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl index 80b67c5e85e..c56a032e57f 100644 --- a/src/test/modules/test_misc/t/013_temp_obj_multisession.pl +++ b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl @@ -56,25 +56,20 @@ # DML and SELECT have to read the table's data and therefore go through # the buffer manager. With no index on the table, the planner cannot # use index access, so SELECT/UPDATE/DELETE/MERGE/COPY all run through -# the read-stream path. -# -# XXX: in current code, the read-stream path bypasses the -# RELATION_IS_OTHER_TEMP() check, so these commands silently see no -# rows / report zero affected rows -- the visible symptom of the bug -# this test suite documents. A follow-up patch will route the check -# through read_stream_begin_impl() and these assertions will be -# updated to expect "cannot access temporary tables of other sessions". +# the read-stream path and are caught by read_stream_begin_impl(). $node->psql( 'postgres', "SELECT val FROM $tempschema.foo;", - stdout => \$stdout, stderr => \$stderr); -is($stderr, '', 'SELECT (currently no error -- bug to be fixed)'); +like( + $stderr, + qr/cannot access temporary tables of other sessions/, + 'SELECT (seqscan via read_stream)'); # INSERT goes through hio.c which calls ReadBufferExtended() to find a -# page with free space; that hits the existing check before any data is -# written. This case currently errors as expected. +# page with free space; that hits the existing check before any data +# is written. $node->psql( 'postgres', "INSERT INTO $tempschema.foo VALUES (73);", @@ -88,21 +83,21 @@ 'postgres', "UPDATE $tempschema.foo SET val = NULL;", stderr => \$stderr); -is($stderr, '', 'UPDATE (currently no error -- bug to be fixed)'); +like($stderr, qr/cannot access temporary tables of other sessions/, 'UPDATE'); $node->psql('postgres', "DELETE FROM $tempschema.foo;", stderr => \$stderr); -is($stderr, '', 'DELETE (currently no error -- bug to be fixed)'); +like($stderr, qr/cannot access temporary tables of other sessions/, 'DELETE'); $node->psql( 'postgres', "MERGE INTO $tempschema.foo USING (VALUES (42)) AS s(val) " . "ON foo.val = s.val WHEN MATCHED THEN DELETE;", stderr => \$stderr); -is($stderr, '', 'MERGE (currently no error -- bug to be fixed)'); +like($stderr, qr/cannot access temporary tables of other sessions/, 'MERGE'); $node->psql('postgres', "COPY $tempschema.foo TO STDOUT;", stderr => \$stderr); -is($stderr, '', 'COPY (currently no error -- bug to be fixed)'); +like($stderr, qr/cannot access temporary tables of other sessions/, 'COPY'); # DDL and maintenance commands have their own command-specific checks # (older than the buffer-manager check above), so they fail with From ed0c4d5af2ef4b3d0880aba768fe7948f3e3e1e6 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Thu, 14 May 2026 13:11:49 -0500 Subject: [PATCH 006/250] refint: Fix segfault in check_foreign_key(). When an UPDATE statement triggers check_foreign_key() with the action set to "cascade", it generates more UPDATE statements to modify the key values in referencing relations. If a new key value is NULL, SPI_getvalue() returns a NULL pointer, which is subsequently passed to quote_literal_cstr(), causing a segfault. To fix, skip quoting when a new key value is NULL and insert an unquoted NULL keyword instead. Oversight in commit 260e97733b. While the refint documentation recommends marking primary key columns NOT NULL, the aforementioned scenario accidentally worked on platforms where snprintf() substitutes "(null)" for NULL pointers. Note that for character-type columns, the old code quoted "(null)" as a string literal, so this didn't always produce correct results. But it still seems better to fix this than to reject cases that previously worked. Reported-by: Nikita Kalinin Author: Ayush Tiwari Reviewed-by: Pierre Forstmann Discussion: https://postgr.es/m/19476-bd04ea6241345303%40postgresql.org Backpatch-through: 14 --- contrib/spi/refint.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/spi/refint.c b/contrib/spi/refint.c index ab0b2b291f9..4877614dfcb 100644 --- a/contrib/spi/refint.c +++ b/contrib/spi/refint.c @@ -487,7 +487,8 @@ check_foreign_key(PG_FUNCTION_ARGS) nv = SPI_getvalue(newtuple, tupdesc, fn); appendStringInfo(&sql, " %s = %s ", - args2[k], quote_literal_cstr(nv)); + args2[k], + nv ? quote_literal_cstr(nv) : "NULL"); if (k < nkeys) appendStringInfoString(&sql, ", "); } From f45f418275b14fab6a074fee7081a89287b9149f Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 15 May 2026 18:02:47 +0900 Subject: [PATCH 007/250] Re-add regression tests for ltree and intarray These tests have been removed by 906ea101d0d5, due to some of them being unstable in the buildfarm with low max_stack_depth values. They are now reworked so as they should be more portable. The tests to cover the findoprnd() overflows use a balanced tree to avoid using too much stack, per a suggestion and an investigation by Tom Lane. Note: This is initially applied only on HEAD; a backpatch will follow should the buildfarm be fine with the situation. Discussion: https://postgr.es/m/agZc6XecyE7E7fep@paquier.xyz Backpatch-through: 14 --- contrib/intarray/expected/_int.out | 15 +++++++++++++++ contrib/intarray/sql/_int.sql | 13 +++++++++++++ contrib/ltree/expected/ltree.out | 24 ++++++++++++++++++++++++ contrib/ltree/sql/ltree.sql | 20 ++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/contrib/intarray/expected/_int.out b/contrib/intarray/expected/_int.out index d0e68d0447f..fb4086a95ca 100644 --- a/contrib/intarray/expected/_int.out +++ b/contrib/intarray/expected/_int.out @@ -398,6 +398,21 @@ SELECT '1&(2&(4&(5|!6)))'::query_int; 1 & 2 & 4 & ( 5 | !6 ) (1 row) +-- Test for overflow of the int16 "left" field in findoprnd(). +-- This query uses a balanced binary tree to avoid using too much stack. +DO $$ +DECLARE + e text := '1'; +BEGIN + FOR i IN 1..15 LOOP + e := '(' || e || '&' || e || ')'; + END LOOP; + PERFORM ('0|' || e)::query_int; +END; +$$; +ERROR: query_int expression is too complex +CONTEXT: SQL statement "SELECT ('0|' || e)::query_int" +PL/pgSQL function inline_code_block line 8 at PERFORM -- test non-error-throwing input SELECT str as "query_int", pg_input_is_valid(str,'query_int') as ok, diff --git a/contrib/intarray/sql/_int.sql b/contrib/intarray/sql/_int.sql index 5668ab40704..0d30914725e 100644 --- a/contrib/intarray/sql/_int.sql +++ b/contrib/intarray/sql/_int.sql @@ -75,6 +75,19 @@ SELECT '1&2&4&5&6'::query_int; SELECT '1&(2&(4&(5|6)))'::query_int; SELECT '1&(2&(4&(5|!6)))'::query_int; +-- Test for overflow of the int16 "left" field in findoprnd(). +-- This query uses a balanced binary tree to avoid using too much stack. +DO $$ +DECLARE + e text := '1'; +BEGIN + FOR i IN 1..15 LOOP + e := '(' || e || '&' || e || ')'; + END LOOP; + PERFORM ('0|' || e)::query_int; +END; +$$; + -- test non-error-throwing input SELECT str as "query_int", diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out index c8eac3f6b21..108df668bf7 100644 --- a/contrib/ltree/expected/ltree.out +++ b/contrib/ltree/expected/ltree.out @@ -1281,6 +1281,21 @@ SELECT 'tree.awdfg_qwerty'::ltree @ 'tree & aw_rw%*'::ltxtquery; f (1 row) +-- Test for overflow of the int16 "left" field in findoprnd(). +-- This query uses a balanced binary tree to avoid using too much stack. +DO $$ +DECLARE + e text := 'a'; +BEGIN + FOR i IN 1..14 LOOP + e := '(' || e || '&' || e || ')'; + END LOOP; + PERFORM ('b|' || e)::ltxtquery; +END; +$$; +ERROR: ltxtquery is too large +CONTEXT: SQL statement "SELECT ('b|' || e)::ltxtquery" +PL/pgSQL function inline_code_block line 8 at PERFORM --arrays SELECT '{1.2.3}'::ltree[] @> '1.2.3.4'; ?column? @@ -8200,3 +8215,12 @@ FROM (VALUES ('.2.3', 'ltree'), !tree & aWdf@* | ltxtquery | t | | | | (8 rows) +-- Test for overflow of lquery_level.totallen. +SELECT (repeat('x', 255) || repeat('|' || repeat('x', 255), 256))::lquery; +ERROR: lquery level is too large +DETAIL: Total size of level exceeds the maximum allowed (65535 bytes). +--- Test for overflow of lquery_level.numvar, with a set of single-char +--- variants in one level. +SELECT (repeat('a|', 65535) || 'a')::lquery; +ERROR: lquery level has too many variants +DETAIL: Number of variants exceeds the maximum allowed (65535). diff --git a/contrib/ltree/sql/ltree.sql b/contrib/ltree/sql/ltree.sql index dd705d9d7ca..c450ccdb43d 100644 --- a/contrib/ltree/sql/ltree.sql +++ b/contrib/ltree/sql/ltree.sql @@ -252,6 +252,19 @@ SELECT 'tree.awdfg'::ltree @ 'tree & aWdfg@'::ltxtquery; SELECT 'tree.awdfg_qwerty'::ltree @ 'tree & aw_qw%*'::ltxtquery; SELECT 'tree.awdfg_qwerty'::ltree @ 'tree & aw_rw%*'::ltxtquery; +-- Test for overflow of the int16 "left" field in findoprnd(). +-- This query uses a balanced binary tree to avoid using too much stack. +DO $$ +DECLARE + e text := 'a'; +BEGIN + FOR i IN 1..14 LOOP + e := '(' || e || '&' || e || ')'; + END LOOP; + PERFORM ('b|' || e)::ltxtquery; +END; +$$; + --arrays SELECT '{1.2.3}'::ltree[] @> '1.2.3.4'; @@ -456,3 +469,10 @@ FROM (VALUES ('.2.3', 'ltree'), ('!tree & aWdf@*','ltxtquery')) AS a(str,typ), LATERAL pg_input_error_info(a.str, a.typ) as errinfo; + +-- Test for overflow of lquery_level.totallen. +SELECT (repeat('x', 255) || repeat('|' || repeat('x', 255), 256))::lquery; + +--- Test for overflow of lquery_level.numvar, with a set of single-char +--- variants in one level. +SELECT (repeat('a|', 65535) || 'a')::lquery; From d472bf14f2595926a96bf668936b2cc920b9a7d3 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Fri, 15 May 2026 18:32:33 -0400 Subject: [PATCH 008/250] Doc: fix release-note typo. This mention of memcpy() should of course have said memcmp(). Reported-by: chris@chrullrich.net Author: Tom Lane Discussion: https://postgr.es/m/177883653690.764749.14038057906859461991@wrigleys.postgresql.org Backpatch-through: 14 --- doc/src/sgml/release-18.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/release-18.sgml b/doc/src/sgml/release-18.sgml index 9537f1932ec..222d884831b 100644 --- a/doc/src/sgml/release-18.sgml +++ b/doc/src/sgml/release-18.sgml @@ -369,7 +369,7 @@ Branch: REL_14_STABLE [b282280e9] 2026-05-11 05:13:51 -0700 Use timingsafe_bcmp() instead - of memcpy() or strcmp() + of memcmp() or strcmp() when checking passwords, hashes, etc. It is not known whether the data dependency of those functions is usefully exploitable in any of these places, but in the interests of safety, replace them. From dc3db3a8349bd164354ef5b6c94d1c0c2adc651c Mon Sep 17 00:00:00 2001 From: Noah Misch Date: Sat, 16 May 2026 18:01:35 -0700 Subject: [PATCH 009/250] Use ereport(ERROR), not Assert(), for publisher tuples missing columns. Three locations use Assert() to guard against a mismatch between the number of columns advertised in the RELATION message and the number actually received in the subsequent INSERT/UPDATE tuple message. Since these values originate from the publisher, the check must survive into production builds. A malicious or buggy publisher can send a RELATION claiming N columns and an INSERT claiming M < N columns. The subscriber's apply worker indexes into colvalues[]/colstatus[] using column indices from the RELATION message's attribute map, causing a heap out-of-bounds read when the tuple's column array is smaller than expected. We've looked, without success, for a scenario in which the publisher holds sufficient control over these out-of-bounds bytes to exploit this or even to reach a SIGSEGV. Despite not finding one, the code has been fragile. Back-patch to v14 (all supported versions). Reported-by: Varik Matevosyan Author: Varik Matevosyan Discussion: https://postgr.es/m/CA+bBoog3cCogktzfLb9bppUByu-10B3CFp8u=iKXG_OvtAguCw@mail.gmail.com Backpatch-through: 14 --- src/backend/replication/logical/worker.c | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 2d39a8812f1..033d00e6d61 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -805,9 +805,15 @@ slot_store_data(TupleTableSlot *slot, LogicalRepRelMapEntry *rel, if (!att->attisdropped && remoteattnum >= 0) { - StringInfo colvalue = &tupleData->colvalues[remoteattnum]; + StringInfo colvalue; + + if (remoteattnum >= tupleData->ncols) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("logical replication column %d not found in tuple: only %d column(s) received", + remoteattnum + 1, tupleData->ncols))); - Assert(remoteattnum < tupleData->ncols); + colvalue = &tupleData->colvalues[remoteattnum]; /* Set attnum for error callback */ apply_error_callback_arg.remote_attnum = remoteattnum; @@ -918,7 +924,11 @@ slot_modify_data(TupleTableSlot *slot, TupleTableSlot *srcslot, if (remoteattnum < 0) continue; - Assert(remoteattnum < tupleData->ncols); + if (remoteattnum >= tupleData->ncols) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("logical replication column %d not found in tuple: only %d column(s) received", + remoteattnum + 1, tupleData->ncols))); if (tupleData->colstatus[remoteattnum] != LOGICALREP_COLUMN_UNCHANGED) { @@ -2618,7 +2628,12 @@ apply_handle_update(StringInfo s) if (!att->attisdropped && remoteattnum >= 0) { - Assert(remoteattnum < newtup.ncols); + if (remoteattnum >= newtup.ncols) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("logical replication column %d not found in tuple: only %d column(s) received", + remoteattnum + 1, newtup.ncols))); + if (newtup.colstatus[remoteattnum] != LOGICALREP_COLUMN_UNCHANGED) target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols, From 20a4b06a1ea1396ab1ced0db96406b50fb3b603a Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 18 May 2026 11:11:44 +0900 Subject: [PATCH 010/250] injection_points: Move some structs to new header injection_points.h This commit moves the definitions of InjectionPointConditionType and InjectionPointCondition into a new header local to the test module injection_points.h, so as these can be shared across more files in the module. A patch for a bug fix is under discussion, whose proposed test will benefit from this refactoring. Backpatch down to where the module exists, as this should be useful for future bug fixes, even cases unrelated to the thread where this change has been discussed. Author: Andrey Borodin Author: Vlad Lesin Discussion: https://postgr.es/m/d2983796-2603-41b7-a66e-fc8489ddb954@gmail.com Backpatch-through: 17 --- .../injection_points/injection_points.c | 25 +------------- .../injection_points/injection_points.h | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 src/test/modules/injection_points/injection_points.h diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c index 3da0cbc10e0..71b1bd0473f 100644 --- a/src/test/modules/injection_points/injection_points.c +++ b/src/test/modules/injection_points/injection_points.c @@ -18,6 +18,7 @@ #include "postgres.h" #include "fmgr.h" +#include "injection_points.h" #include "injection_stats.h" #include "miscadmin.h" #include "nodes/pg_list.h" @@ -39,30 +40,6 @@ PG_MODULE_MAGIC; #define INJ_MAX_WAIT 8 #define INJ_NAME_MAXLEN 64 -/* - * Conditions related to injection points. This tracks in shared memory the - * runtime conditions under which an injection point is allowed to run, - * stored as private_data when an injection point is attached, and passed as - * argument to the callback. - * - * If more types of runtime conditions need to be tracked, this structure - * should be expanded. - */ -typedef enum InjectionPointConditionType -{ - INJ_CONDITION_ALWAYS = 0, /* always run */ - INJ_CONDITION_PID, /* PID restriction */ -} InjectionPointConditionType; - -typedef struct InjectionPointCondition -{ - /* Type of the condition */ - InjectionPointConditionType type; - - /* ID of the process where the injection point is allowed to run */ - int pid; -} InjectionPointCondition; - /* * List of injection points stored in TopMemoryContext attached * locally to this process. diff --git a/src/test/modules/injection_points/injection_points.h b/src/test/modules/injection_points/injection_points.h new file mode 100644 index 00000000000..caabc4ffb32 --- /dev/null +++ b/src/test/modules/injection_points/injection_points.h @@ -0,0 +1,33 @@ +/*------------------------------------------------------------------------- + * + * injection_points.h + * Definitions for the injection points module + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/test/modules/injection_points/injection_points.h + * + *------------------------------------------------------------------------- + */ + +#ifndef INJECTION_POINTS_H +#define INJECTION_POINTS_H + +typedef enum InjectionPointConditionType +{ + INJ_CONDITION_ALWAYS = 0, /* always run */ + INJ_CONDITION_PID, /* PID restriction */ +} InjectionPointConditionType; + +typedef struct InjectionPointCondition +{ + /* Type of the condition */ + InjectionPointConditionType type; + + /* ID of the process where the injection point is allowed to run */ + int pid; +} InjectionPointCondition; + +#endif /* INJECTION_POINTS_H */ From e0c641ebbf0aab219bbe5fe0ed2be4d937f1fea1 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Mon, 18 May 2026 08:33:36 -0700 Subject: [PATCH 011/250] psql: Make ParseVariableDouble reject values above max ParseVariableDouble missed returning false after logging an error when the parsed value exceeded max, making the value assigned rather than rejected. Backpatch down to v18 where this was introduced as part of the \WATCH_INTERVAL. Author: Sven Klemm Co-authored-by: Daniel Gustafsson Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/CAMCrgp31p_5SDVi7dwnP39tTW5icQ0MWHA+N4kJdXgkL0PEy8w@mail.gmail.com Backpatch-through: 18 --- src/bin/psql/t/001_basic.pl | 2 ++ src/bin/psql/variables.c | 1 + 2 files changed, 3 insertions(+) diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl index cf07a9dbd5e..6f2341fcaa1 100644 --- a/src/bin/psql/t/001_basic.pl +++ b/src/bin/psql/t/001_basic.pl @@ -451,6 +451,8 @@ sub psql_fails_like '\set WATCH_INTERVAL 1e500', qr/is out of range/, 'WATCH_INTERVAL variable is out of range'); +psql_like($node, '\echo :WATCH_INTERVAL', + qr/^2$/m, 'WATCH_INTERVAL variable was not altered'); # Test \g output piped into a program. # The program is perl -pe '' to simply copy the input to the output. diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c index 6b64302ebca..07f5e3f6f2d 100644 --- a/src/bin/psql/variables.c +++ b/src/bin/psql/variables.c @@ -224,6 +224,7 @@ ParseVariableDouble(const char *value, const char *name, double *result, double if (name) pg_log_error("invalid value \"%s\" for variable \"%s\": must be less than %.2f", value, name, max); + return false; } *result = dblval; return true; From 89b4b3ae35d2f38aa0e12b147c92c99262c6d146 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 20 May 2026 15:54:13 +0900 Subject: [PATCH 012/250] pg_recvlogical: Honor source cluster file permissions for output files Commit c37b3d08ca6 attempted to preserve group permissions on pg_recvlogical output files when group access was enabled on the source cluster. However, the output files were still created with a fixed S_IRUSR | S_IWUSR mode, preventing group-read permissions from being applied. This commit fixes the issue by creating output files with pg_file_create_mode instead of a hard-coded mode. This allows pg_recvlogical to correctly preserve group permissions from the source cluster. Backpatch to all supported branches. Author: Fujii Masao Reviewed-by: Srinath Reddy Sadipiralla Discussion: https://postgr.es/m/CAHGQGwHhpizYzMo3nFP4GkNMueSNMY3QfC-gBN1VTXtuiANDvw@mail.gmail.com Backpatch-through: 14 --- doc/src/sgml/ref/pg_recvlogical.sgml | 2 +- src/bin/pg_basebackup/pg_recvlogical.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/ref/pg_recvlogical.sgml b/doc/src/sgml/ref/pg_recvlogical.sgml index 263ebdeeab4..c3d641905bb 100644 --- a/doc/src/sgml/ref/pg_recvlogical.sgml +++ b/doc/src/sgml/ref/pg_recvlogical.sgml @@ -492,7 +492,7 @@ PostgreSQL documentation pg_recvlogical will preserve group permissions on - the received WAL files if group permissions are enabled on the source + the output files if group permissions are enabled on the source cluster. diff --git a/src/bin/pg_basebackup/pg_recvlogical.c b/src/bin/pg_basebackup/pg_recvlogical.c index fb7a6a1d05d..1e8b149d4e7 100644 --- a/src/bin/pg_basebackup/pg_recvlogical.c +++ b/src/bin/pg_basebackup/pg_recvlogical.c @@ -342,7 +342,7 @@ StreamLogicalLog(void) outfd = fileno(stdout); else outfd = open(outfile, O_CREAT | O_APPEND | O_WRONLY | PG_BINARY, - S_IRUSR | S_IWUSR); + pg_file_create_mode); if (outfd == -1) { pg_log_error("could not open log file \"%s\": %m", outfile); From 41247cdf695b99eb4b6359ef3d6bdcfbad321847 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 22 May 2026 23:59:04 +0900 Subject: [PATCH 013/250] Prevent setting NO INHERIT on partitioned NOT NULL constraints The documentation states that NOT NULL constraints on partitioned tables are always inherited by all partitions, and therefore cannot be declared NO INHERIT. While a check already existed to reject creating such constraints with NO INHERIT, previously the same check was missing for ALTER TABLE ... ALTER CONSTRAINT ... NO INHERIT. This commit adds the missing check so that attempting to set NO INHERIT on a partitioned NOT NULL constraint now fails. Backpatch to v18, where ALTER TABLE ... ALTER CONSTRAINT ... [NO] INHERIT was added. Author: Andreas Karlsson Reviewed-by: Jim Jones Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/ecc985ad-6ec1-4094-a315-317943ca5f3f@proxel.se Backpatch-through: 18 --- src/backend/commands/tablecmds.c | 6 ++++++ src/test/regress/expected/constraints.out | 4 ++++ src/test/regress/sql/constraints.sql | 3 +++ 3 files changed, 13 insertions(+) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index cabe22d48be..5307157f0f3 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -12262,6 +12262,12 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon, errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("constraint \"%s\" of relation \"%s\" is not a not-null constraint", cmdcon->conname, RelationGetRelationName(rel))); + if (cmdcon->alterInheritability && + cmdcon->noinherit && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("not-null constraint \"%s\" on partitioned table \"%s\" cannot be NO INHERIT", + cmdcon->conname, RelationGetRelationName(rel))); /* Refuse to modify inheritability of inherited constraints */ if (cmdcon->alterInheritability && diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index ebc892a2a42..e619459647c 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -1042,6 +1042,10 @@ CREATE TABLE ATACC1 (a int NOT NULL NO INHERIT) PARTITION BY LIST (a); ERROR: not-null constraints on partitioned tables cannot be NO INHERIT CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT) PARTITION BY LIST (a); ERROR: not-null constraints on partitioned tables cannot be NO INHERIT +CREATE TABLE ATACC1 (a int, CONSTRAINT a_is_not_null NOT NULL a) PARTITION BY LIST (a); +ALTER TABLE ATACC1 ALTER CONSTRAINT a_is_not_null NO INHERIT; +ERROR: not-null constraint "a_is_not_null" on partitioned table "atacc1" cannot be NO INHERIT +DROP TABLE ATACC1; -- it's not possible to override a no-inherit constraint with an inheritable one CREATE TABLE ATACC2 (a int, CONSTRAINT a_is_not_null NOT NULL a NO INHERIT); CREATE TABLE ATACC1 (a int); diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index 1e9989698b6..99846e7cc6a 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -702,6 +702,9 @@ DROP TABLE ATACC1, ATACC2, ATACC3; -- NOT NULL NO INHERIT is not possible on partitioned tables CREATE TABLE ATACC1 (a int NOT NULL NO INHERIT) PARTITION BY LIST (a); CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT) PARTITION BY LIST (a); +CREATE TABLE ATACC1 (a int, CONSTRAINT a_is_not_null NOT NULL a) PARTITION BY LIST (a); +ALTER TABLE ATACC1 ALTER CONSTRAINT a_is_not_null NO INHERIT; +DROP TABLE ATACC1; -- it's not possible to override a no-inherit constraint with an inheritable one CREATE TABLE ATACC2 (a int, CONSTRAINT a_is_not_null NOT NULL a NO INHERIT); From b903d17927eecab8dd741eb71f04eee5a1182eff Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Sat, 23 May 2026 08:10:12 +0900 Subject: [PATCH 014/250] Avoid exposing WAL receiver raw conninfo during timeline jumps When reusing an existing WAL receiver after it has reached WALRCV_WAITING for new instructions, RequestXLogStreaming() copied PrimaryConnInfo into WalRcv->conninfo before switching the state to WALRCV_RESTARTING. At that point ready_to_display could still be true, so pg_stat_wal_receiver could expose the raw connection string, including sensitive fields, but it should only show the user-displayable version of the connection string. WALRCV_RESTARTING does not establish a new connection. The waiting WAL receiver reuses its existing connection and only needs a new startpoint and timeline, so there is no need to copy the raw connection string into shared memory again. Let's only copy conninfo when launching a new WAL receiver after WALRCV_STOPPED, not while waiting for instructions. This commit adds coverage for the case fixed by this commit to the timeline-switch test by verifying that the WAL receiver conninfo remains consistent across the jump. Backpatch all the way down, as this issue is possible since pg_stat_wal_receiver has been introduced. Author: Chao Li Reviewed-by: Michael Paquier Discussion: https://postgr.es/m/EF91FF76-1E2B-4F3B-9162-290B4DC517FF@gmail.com Backpatch-through: 14 --- src/backend/replication/walreceiverfuncs.c | 14 +++++++++----- src/test/recovery/t/004_timeline_switch.pl | 15 +++++++++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c index 8de2886ff0b..7ef07fb4712 100644 --- a/src/backend/replication/walreceiverfuncs.c +++ b/src/backend/replication/walreceiverfuncs.c @@ -266,11 +266,6 @@ RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr, const char *conninfo, Assert(walrcv->walRcvState == WALRCV_STOPPED || walrcv->walRcvState == WALRCV_WAITING); - if (conninfo != NULL) - strlcpy(walrcv->conninfo, conninfo, MAXCONNINFO); - else - walrcv->conninfo[0] = '\0'; - /* * Use configured replication slot if present, and ignore the value of * create_temp_slot as the slot name should be persistent. Otherwise, use @@ -288,10 +283,19 @@ RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr, const char *conninfo, walrcv->is_temp_slot = create_temp_slot; } + /* + * While waiting for instructions, the WAL receiver uses the same + * connection, so do not clobber the user-visible conninfo already saved. + */ if (walrcv->walRcvState == WALRCV_STOPPED) { launch = true; walrcv->walRcvState = WALRCV_STARTING; + + if (conninfo != NULL) + strlcpy(walrcv->conninfo, conninfo, MAXCONNINFO); + else + walrcv->conninfo[0] = '\0'; } else walrcv->walRcvState = WALRCV_RESTARTING; diff --git a/src/test/recovery/t/004_timeline_switch.pl b/src/test/recovery/t/004_timeline_switch.pl index 5c2f8665330..a6e2e2d4c37 100644 --- a/src/test/recovery/t/004_timeline_switch.pl +++ b/src/test/recovery/t/004_timeline_switch.pl @@ -47,11 +47,15 @@ stdout => \$psql_out); is($psql_out, 't', "promotion of standby with pg_promote"); -# Switch standby 2 to replay from standby 1 +# Switch standby 2 to replay from standby 1. During the timeline switch, +# the WAL receiver process on standby 2 should not be stopped, and the +# new primary connection string should not be visible +# in pg_stat_wal_receiver. +my $secret = 'dont_show_me'; my $connstr_1 = $node_standby_1->connstr; $node_standby_2->append_conf( 'postgresql.conf', qq( -primary_conninfo='$connstr_1' +primary_conninfo='$connstr_1 password=$secret' )); # Rotate logfile before restarting, for the log checks done below. @@ -93,6 +97,13 @@ is($wr_pid_before_switch, $wr_pid_after_switch, 'WAL receiver PID matches across timeline jumps'); +my $raw_conninfo_count = $node_standby_2->safe_psql('postgres', + "SELECT count(*) FROM pg_stat_wal_receiver WHERE conninfo LIKE '%$secret%'" +); + +is($raw_conninfo_count, '0', + 'pg_stat_wal_receiver.conninfo not updated across timeline jumps'); + # Ensure that a standby is able to follow a primary on a newer timeline # when WAL archiving is enabled. From b5fd5723a6594f0d74cc05ce5971190b71efdd20 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 25 May 2026 14:38:59 +0900 Subject: [PATCH 015/250] Fix size check in statext_dependencies_deserialize() The check for the minimum expected bytea size of a MVDependencies object was using SizeOfItem() for its calculation. This macro uses the number of attributes in a single dependency. This minimum size calculation should be based on MinSizeOfItems(), that computes the minimum expected size as the header plus the minimally-sized number of dependency items. Oversight in d08c44f7a4ec. Author: Ilia Evdokimov Discussion: https://postgr.es/m/4b8d299d-2505-4c30-bf80-0f697410db35@tantorlabs.com Backpatch-through: 14 --- src/backend/statistics/dependencies.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c index eb2fc4366b4..fc605aa7526 100644 --- a/src/backend/statistics/dependencies.c +++ b/src/backend/statistics/dependencies.c @@ -536,7 +536,7 @@ statext_dependencies_deserialize(bytea *data) elog(ERROR, "invalid zero-length item array in MVDependencies"); /* what minimum bytea size do we expect for those parameters */ - min_expected_size = SizeOfItem(dependencies->ndeps); + min_expected_size = MinSizeOfItems(dependencies->ndeps); if (VARSIZE_ANY_EXHDR(data) < min_expected_size) elog(ERROR, "invalid dependencies size %zu (expected at least %zu)", From 88d7748d2ab7017c29c0f7bec04612b2680552d0 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 26 May 2026 00:46:31 +0900 Subject: [PATCH 016/250] postgres_fdw: Give user mapping precedence for use_scram_passthrough Previously, when use_scram_passthrough was specified on both a foreign server and a user mapping, the server-level setting took precedence over the user-mapping setting. This was inconsistent with the usual semantics of postgres_fdw options, where foreign server options provide shared defaults and user mapping options override them on a per-user basis. This commit updates postgres_fdw so that the user-mapping setting takes precedence when use_scram_passthrough is specified in both places. This matches the behavior of other connection options such as sslcert and sslkey. Backpatch to v18, where use_scram_passthrough was introduced. In v18, this only affects limited configurations that specify conflicting values at both the foreign server and user-mapping levels. In such cases, users would naturally expect the user-mapping setting to override the server-level setting, so changing the behavior should be minimally disruptive. Also keeping v18 as the only branch with different semantics for use_scram_passthrough would be unnecessarily confusing, so backpatch this fix to v18. Author: Matheus Alcantara Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAHGQGwEJ8rZjmbOvCicyr4vbuLio082bNTde0WNoSWaWr9wVcg@mail.gmail.com Backpatch-through: 18 --- contrib/postgres_fdw/connection.c | 10 ++++++-- contrib/postgres_fdw/t/001_auth_scram.pl | 30 ++++++++++++++++++++++++ doc/src/sgml/postgres-fdw.sgml | 4 +++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index 776866f0015..1d3d86f2494 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -680,12 +680,18 @@ UserMappingPasswordRequired(UserMapping *user) return true; } +/* + * Return whether SCRAM pass-through is enabled. + * + * If use_scram_passthrough is specified in both the foreign server + * and the user mapping, the user mapping setting takes precedence. + */ static bool UseScramPassthrough(ForeignServer *server, UserMapping *user) { ListCell *cell; - foreach(cell, server->options) + foreach(cell, user->options) { DefElem *def = (DefElem *) lfirst(cell); @@ -693,7 +699,7 @@ UseScramPassthrough(ForeignServer *server, UserMapping *user) return defGetBoolean(def); } - foreach(cell, user->options) + foreach(cell, server->options) { DefElem *def = (DefElem *) lfirst(cell); diff --git a/contrib/postgres_fdw/t/001_auth_scram.pl b/contrib/postgres_fdw/t/001_auth_scram.pl index b94a6a6293b..e41d328ff85 100644 --- a/contrib/postgres_fdw/t/001_auth_scram.pl +++ b/contrib/postgres_fdw/t/001_auth_scram.pl @@ -20,6 +20,7 @@ my $db2 = "db2"; # For node2 my $fdw_server = "db1_fdw"; my $fdw_server2 = "db2_fdw"; +my $fdw_server3 = "db1_fdw_override"; my $node1 = PostgreSQL::Test::Cluster->new('node1'); my $node2 = PostgreSQL::Test::Cluster->new('node2'); @@ -46,9 +47,11 @@ $node1->safe_psql($db0, 'CREATE EXTENSION IF NOT EXISTS postgres_fdw'); setup_fdw_server($node1, $db0, $fdw_server, $node1, $db1); setup_fdw_server($node1, $db0, $fdw_server2, $node2, $db2); +setup_fdw_server($node1, $db0, $fdw_server3, $node1, $db1); setup_user_mapping($node1, $db0, $fdw_server); setup_user_mapping($node1, $db0, $fdw_server2); +setup_user_mapping($node1, $db0, $fdw_server3); # Make the user have the same SCRAM key on both servers. Forcing to have the # same iteration and salt. @@ -68,6 +71,33 @@ test_auth($node2, $db2, "t2", "SCRAM auth directly on foreign server should still succeed"); +# Test that use_scram_passthrough=false on user mapping overrides server setting +{ + my $connstr = $node1->connstr($db0) . qq' user=$user'; + + $node1->safe_psql($db0, + qq'ALTER USER MAPPING FOR $user SERVER $fdw_server3 OPTIONS(add use_scram_passthrough \'false\')', + 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); + + my ($ret, $stdout, $stderr) = $node1->psql( + $db0, + qq'SELECT count(1) FROM override_t', + 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'); +} + SKIP: { skip "test requires Unix-domain sockets", 4 if !$use_unix_sockets; diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index 781a01067f7..d9f6efbbdaf 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -776,7 +776,9 @@ OPTIONS (ADD password_required 'false'); This option controls whether postgres_fdw will use the SCRAM pass-through authentication to connect to the foreign - server. With SCRAM pass-through authentication, + server. It can be specified for a foreign server or a user mapping. + A user mapping setting overrides the foreign server setting. + With SCRAM pass-through authentication, postgres_fdw uses SCRAM-hashed secrets instead of plain-text user passwords to connect to the remote server. This avoids storing plain-text user passwords in PostgreSQL system From 130396e6c034c26617c8a73be44f9640082fd490 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 26 May 2026 00:51:18 +0900 Subject: [PATCH 017/250] dblink: Give user mapping precedence for use_scram_passthrough Commit 97f6fc10fff changed postgres_fdw so that user-mapping settings override foreign server settings for use_scram_passthrough. This commit applies the same behavior to dblink. Backpatch to v18, where use_scram_passthrough was introduced. Author: Matheus Alcantara Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAHGQGwEJ8rZjmbOvCicyr4vbuLio082bNTde0WNoSWaWr9wVcg@mail.gmail.com Backpatch-through: 18 --- contrib/dblink/dblink.c | 10 ++++++++-- contrib/dblink/t/001_auth_scram.pl | 24 ++++++++++++++++++++++++ doc/src/sgml/dblink.sgml | 10 ++++++---- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c index ec0c832e921..4112727fe54 100644 --- a/contrib/dblink/dblink.c +++ b/contrib/dblink/dblink.c @@ -3248,12 +3248,18 @@ appendSCRAMKeysInfo(StringInfo buf) } +/* + * Return whether SCRAM pass-through is enabled. + * + * If use_scram_passthrough is specified in both the foreign server + * and the user mapping, the user mapping setting takes precedence. + */ static bool UseScramPassthrough(ForeignServer *foreign_server, UserMapping *user) { ListCell *cell; - foreach(cell, foreign_server->options) + foreach(cell, user->options) { DefElem *def = lfirst(cell); @@ -3261,7 +3267,7 @@ UseScramPassthrough(ForeignServer *foreign_server, UserMapping *user) return defGetBoolean(def); } - foreach(cell, user->options) + foreach(cell, foreign_server->options) { DefElem *def = (DefElem *) lfirst(cell); diff --git a/contrib/dblink/t/001_auth_scram.pl b/contrib/dblink/t/001_auth_scram.pl index ef3dea6c5ad..0a37f2d06f8 100644 --- a/contrib/dblink/t/001_auth_scram.pl +++ b/contrib/dblink/t/001_auth_scram.pl @@ -24,6 +24,7 @@ my $db2 = "db2"; # For node2 my $fdw_server = "db1_fdw"; my $fdw_server2 = "db2_fdw"; +my $fdw_server3 = "db1_fdw_override"; my $fdw_invalid_server = "db2_fdw_invalid"; # For invalid fdw options my $fdw_invalid_server2 = "db2_fdw_invalid2"; # For invalid scram keys fdw options @@ -55,10 +56,12 @@ setup_fdw_server($node1, $db0, $fdw_server2, $node2, $db2); setup_invalid_fdw_server($node1, $db0, $fdw_invalid_server, $node2, $db2); setup_fdw_server($node1, $db0, $fdw_invalid_server2, $node2, $db2); +setup_fdw_server($node1, $db0, $fdw_server3, $node1, $db1); setup_user_mapping($node1, $db0, $fdw_server); setup_user_mapping($node1, $db0, $fdw_server2); setup_user_mapping($node1, $db0, $fdw_invalid_server); +setup_user_mapping($node1, $db0, $fdw_server3); # Make the user have the same SCRAM key on both servers. Forcing to have the # same iteration and salt. @@ -96,6 +99,27 @@ test_fdw_auth_with_invalid_overwritten_require_auth($fdw_invalid_server); +# Test that use_scram_passthrough=false on user mapping overrides server setting +{ + my $connstr = $node1->connstr($db0) . qq' user=$user'; + + $node1->safe_psql($db0, + qq'ALTER USER MAPPING FOR $user SERVER $fdw_server3 OPTIONS(add use_scram_passthrough \'false\')', + connstr => $connstr + ); + + my ($ret, $stdout, $stderr) = $node1->psql( + $db0, + "select * from dblink('$fdw_server3', 'select * from t') as t(a int, b int)", + 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'); +} + # Ensure that trust connections fail without superuser opt-in. unlink($node1->data_dir . '/pg_hba.conf'); unlink($node2->data_dir . '/pg_hba.conf'); diff --git a/doc/src/sgml/dblink.sgml b/doc/src/sgml/dblink.sgml index 808c690985b..41713eb5207 100644 --- a/doc/src/sgml/dblink.sgml +++ b/doc/src/sgml/dblink.sgml @@ -154,10 +154,12 @@ dblink_connect(text connname, text connstr) returns text The foreign-data wrapper dblink_fdw has an additional Boolean option use_scram_passthrough that controls whether dblink will use the SCRAM pass-through - authentication to connect to the remote database. With SCRAM pass-through - authentication, dblink uses SCRAM-hashed secrets - instead of plain-text user passwords to connect to the remote server. This - avoids storing plain-text user passwords in PostgreSQL system catalogs. + authentication to connect to the remote database. It can be specified + for a foreign server or a user mapping. A user mapping setting overrides + the foreign server setting. With SCRAM pass-through authentication, + dblink uses SCRAM-hashed secrets instead of plain-text + user passwords to connect to the remote server. This avoids storing + plain-text user passwords in PostgreSQL system catalogs. See the documentation of the equivalent use_scram_passthrough option of postgres_fdw for further details and restrictions. From cd777e27e2038f2c39f0f2d5d68d45c67bdf89e8 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 26 May 2026 01:07:24 +0900 Subject: [PATCH 018/250] dblink: Reject use_scram_passthrough on foreign-data wrappers Previously, dblink accepted the use_scram_passthrough option on foreign-data wrappers via ALTER FOREIGN DATA WRAPPER dblink_fdw OPTIONS, even though the setting had no effect there. use_scram_passthrough should be only meaningful for foreign servers and user mappings, so this commit updates dblink to accept the option only in those contexts. Backpatch to v18, where use_scram_passthrough was introduced. Author: Matheus Alcantara Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAHGQGwEJ8rZjmbOvCicyr4vbuLio082bNTde0WNoSWaWr9wVcg@mail.gmail.com Backpatch-through: 18 --- contrib/dblink/dblink.c | 11 +++++++++-- contrib/dblink/expected/dblink.out | 5 +++++ contrib/dblink/sql/dblink.sql | 4 ++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c index 4112727fe54..2f803e7ed3c 100644 --- a/contrib/dblink/dblink.c +++ b/contrib/dblink/dblink.c @@ -3133,8 +3133,15 @@ static bool is_valid_dblink_fdw_option(const PQconninfoOption *options, const char *option, Oid context) { - if (strcmp(option, "use_scram_passthrough") == 0) - return true; + /* + * These options are only valid for foreign server or user mapping + * contexts + */ + if (context == ForeignServerRelationId || context == UserMappingRelationId) + { + if (strcmp(option, "use_scram_passthrough") == 0) + return true; + } return is_valid_dblink_option(options, option, context); } diff --git a/contrib/dblink/expected/dblink.out b/contrib/dblink/expected/dblink.out index c70c79574fd..1d2759def9e 100644 --- a/contrib/dblink/expected/dblink.out +++ b/contrib/dblink/expected/dblink.out @@ -1220,6 +1220,11 @@ SHOW intervalstyle; postgres (1 row) +-- Check that adding use_scram_passthrough option on an foreign data wrapper is +-- not allowed +ALTER FOREIGN DATA WRAPPER dblink_fdw OPTIONS(add use_scram_passthrough 'true'); +ERROR: invalid option "use_scram_passthrough" +HINT: There are no valid options in this context. -- Clean up GUC-setting tests SELECT dblink_disconnect('myconn'); dblink_disconnect diff --git a/contrib/dblink/sql/dblink.sql b/contrib/dblink/sql/dblink.sql index 365b21036e8..d67a0a5992e 100644 --- a/contrib/dblink/sql/dblink.sql +++ b/contrib/dblink/sql/dblink.sql @@ -635,6 +635,10 @@ FROM dblink_fetch('myconn','error_cursor', 1) AS t(i int); SHOW datestyle; SHOW intervalstyle; +-- Check that adding use_scram_passthrough option on an foreign data wrapper is +-- not allowed +ALTER FOREIGN DATA WRAPPER dblink_fdw OPTIONS(add use_scram_passthrough 'true'); + -- Clean up GUC-setting tests SELECT dblink_disconnect('myconn'); RESET datestyle; From e7544c518ab0943fc85e2a4b44df9f7561ad2d0c Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 25 May 2026 18:15:49 -0400 Subject: [PATCH 019/250] Fix missed ReleaseVariableStats() in intarray's _int_matchsel(). Given a WHERE clause like "int[] @@ query_int" or "query_int ~~ int[]" where the query_int side is a table column having statistics, _int_matchsel() exited without remembering to free the statistics tuple. This would typically lead to warnings about cache refcount leakage, like WARNING: resource was not closed: cache pg_statistic (73), tuple 42/12 has count 1 It's been wrong since this code was added, in commit c6fbe6d6f. Bug: #19492 Reported-by: Man Zeng Author: Man Zeng Reviewed-by: Tom Lane Discussion: https://postgr.es/m/19492-ddcd0e22399ef85a@postgresql.org Backpatch-through: 14 --- contrib/intarray/_int_selfuncs.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contrib/intarray/_int_selfuncs.c b/contrib/intarray/_int_selfuncs.c index 60fd163668f..43f4638a8a6 100644 --- a/contrib/intarray/_int_selfuncs.c +++ b/contrib/intarray/_int_selfuncs.c @@ -151,7 +151,10 @@ _int_matchsel(PG_FUNCTION_ARGS) * query_int. */ if (vardata.vartype != INT4ARRAYOID) + { + ReleaseVariableStats(vardata); PG_RETURN_FLOAT8(DEFAULT_EQ_SEL); + } /* * Can't do anything useful if the something is not a constant, either. From 0480d84ee35f386d3d78b58488303f9f6ba1b4c2 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 26 May 2026 11:58:25 -0400 Subject: [PATCH 020/250] Add stack depth check to QueueFKConstraintValidation(). QueueFKConstraintValidation() recurses through the partition hierarchy to queue child constraint validations and to mark child rows as validated. With a sufficiently deep partition tree, this can result in a stack-overflow crash. Defend against that as we do elsewhere. Bug: #19482 Reported-by: Alexander Lakhin Author: Ayush Tiwari Reviewed-by: Tom Lane Discussion: https://postgr.es/m/19482-4cc37cbf52d55235@postgresql.org Backpatch-through: 18 --- src/backend/commands/tablecmds.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 5307157f0f3..5abe02615a1 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -13011,6 +13011,9 @@ QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, HeapTuple copyTuple; Form_pg_constraint copy_con; + /* since this function recurses, it could be driven to stack overflow */ + check_stack_depth(); + con = (Form_pg_constraint) GETSTRUCT(contuple); Assert(con->contype == CONSTRAINT_FOREIGN); Assert(!con->convalidated); From 97b5c5aaad5dba2144215293d4b8438df693c13b Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 27 May 2026 02:26:50 +0300 Subject: [PATCH 021/250] Skip pg_database.dathasloginevt cleanup on standby EventTriggerOnLogin() tries to clear pg_database.dathasloginevt when the database no longer has any login event triggers but the flag is still set. To make that safe against concurrent flag setters, it takes a conditional AccessExclusiveLock on the database object. On a hot standby, that lock acquisition fails outright with FATAL: cannot acquire lock mode AccessExclusiveLock on database objects while recovery is in progress because LockAcquireExtended() refuses locks stronger than RowExclusiveLock on database objects during recovery. The standby already replays the flag's value from the primary, so the dangling flag is the result of replaying a state in which the primary had already dropped its login event triggers but not yet run a login event trigger pass to clear the flag. Any session connecting to the standby in that window therefore fails to connect. Skip the cleanup on a standby. The flag will be cleared via WAL replay once the primary clears it on its side. Add a recovery TAP test that reproduces the original report: create and drop a login event trigger on the primary in one session, wait for the standby to replay, then verify that a fresh connection to the standby succeeds. Backpatch to v17, where the login event triggers were introduced. Author: Ayush Tiwari Reported-by: Egor Chindyaskin Reviewed-by: Fujii Masao Reviewed-by: Alexander Korotkov Discussion: https://postgr.es/m/19488-d7ccfca2bf6b74b0%40postgresql.org Backpatch-through: 17 --- src/backend/commands/event_trigger.c | 10 +- src/test/recovery/meson.build | 3 +- .../t/053_standby_login_event_trigger.pl | 125 ++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/test/recovery/t/053_standby_login_event_trigger.pl diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 074c4765434..0ff8867d3e4 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -935,8 +935,16 @@ EventTriggerOnLogin(void) * lock to prevent concurrent SetDatabaseHasLoginEventTriggers(), but we * don't want to hang the connection waiting on the lock. Thus, we are * just trying to acquire the lock conditionally. + * + * Skip this on a hot standby: the conditional AccessExclusiveLock on the + * database object would fail with "cannot acquire lock mode ... while + * recovery is in progress", which the caller would surface as a FATAL + * connection error. On a standby, we cannot (and must not) clear the + * pg_database flag ourselves; it will be cleared via WAL replay once the + * primary's next login event trigger run clears it on the primary. */ - else if (ConditionalLockSharedObject(DatabaseRelationId, MyDatabaseId, + else if (!RecoveryInProgress() && + ConditionalLockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessExclusiveLock)) { /* diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build index 52993c32dbb..5245fdde43c 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -56,7 +56,8 @@ tests += { 't/045_archive_restartpoint.pl', 't/046_checkpoint_logical_slot.pl', 't/047_checkpoint_physical_slot.pl', - 't/048_vacuum_horizon_floor.pl' + 't/048_vacuum_horizon_floor.pl', + 't/053_standby_login_event_trigger.pl', ], }, } diff --git a/src/test/recovery/t/053_standby_login_event_trigger.pl b/src/test/recovery/t/053_standby_login_event_trigger.pl new file mode 100644 index 00000000000..81903379092 --- /dev/null +++ b/src/test/recovery/t/053_standby_login_event_trigger.pl @@ -0,0 +1,125 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group +# +# Verify that connecting to a standby still works after a login event +# trigger has been created and dropped on the primary. +# +# CREATE EVENT TRIGGER ... ON login sets pg_database.dathasloginevt to +# true on the primary, but DROP EVENT TRIGGER does not clear it -- the +# next login event trigger pass clears the flag lazily on the primary. +# That dangling flag replicates to the standby. Before the +# RecoveryInProgress() guard in EventTriggerOnLogin(), the standby +# tried to clear the flag itself, which requires AccessExclusiveLock +# on the database object; that lock mode is forbidden during recovery, +# so the new connection died with FATAL. +# +# To keep the test robust the event trigger is set up in a dedicated +# database (regress_login_evt). All synchronisation helpers below -- +# wait_for_replay_catchup() and friends -- connect to "postgres" on +# the primary; if the trigger were created in "postgres" itself, that +# probe connection would enter the cleanup branch on the primary and +# silently clear the flag before the test even runs, making the +# scenario unreproducible. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Set up primary and a streaming standby. +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(allows_streaming => 1); +$primary->start; + +my $backup_name = 'login_evt_backup'; +$primary->backup($backup_name); + +my $standby = PostgreSQL::Test::Cluster->new('standby'); +$standby->init_from_backup($primary, $backup_name, has_streaming => 1); +$standby->start; + +# A dedicated database isolates the dangling dathasloginevt flag from +# any helper that connects to the default "postgres" database. +$primary->safe_psql('postgres', 'CREATE DATABASE regress_login_evt'); +$primary->wait_for_replay_catchup($standby); + +# Sanity check: the standby can connect to the new database before +# the trigger machinery has touched it. +$standby->safe_psql('regress_login_evt', 'SELECT 1'); + +# Create and drop a login event trigger inside the dedicated database +# in a single session. CREATE EVENT TRIGGER sets +# pg_database.dathasloginevt = true for regress_login_evt; mark it +# ENABLE ALWAYS so the scenario matches the original bug report. +# After DROP the flag remains set on disk until a subsequent login on +# the primary clears it; since later helpers only touch the +# "postgres" database, regress_login_evt's flag stays set and +# replicates that way to the standby. +$primary->safe_psql( + 'regress_login_evt', q{ +CREATE FUNCTION init_session() RETURNS event_trigger +LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'init_session'; END $$; +CREATE EVENT TRIGGER init_session ON login + EXECUTE FUNCTION init_session(); +ALTER EVENT TRIGGER init_session ENABLE ALWAYS; +DROP EVENT TRIGGER init_session; +DROP FUNCTION init_session(); +}); + +# Wait for the standby to replay the CREATE/DROP catalog state. This +# probes "postgres", not regress_login_evt, so it does not disturb +# the dangling flag. +$primary->wait_for_replay_catchup($standby); + +# The flag remains set in regress_login_evt on both sides. +is( $primary->safe_psql( + 'postgres', + "SELECT dathasloginevt FROM pg_database WHERE datname = 'regress_login_evt'" + ), + 't', + 'dathasloginevt remains set on primary after DROP EVENT TRIGGER'); +is( $standby->safe_psql( + 'postgres', + "SELECT dathasloginevt FROM pg_database WHERE datname = 'regress_login_evt'" + ), + 't', + 'dathasloginevt replicated to standby'); + +# A new connection to regress_login_evt on the standby exercises +# EventTriggerOnLogin()'s cleanup branch. With the +# RecoveryInProgress() guard it succeeds; without it the session +# aborts with a FATAL about AccessExclusiveLock. +my ($ret, $stdout, $stderr) = $standby->psql('regress_login_evt', 'SELECT 1'); +is($ret, 0, + 'standby accepts connection to database with dangling dathasloginevt'); +unlike( + $stderr, + qr/cannot acquire lock mode AccessExclusiveLock/, + 'no AccessExclusiveLock FATAL on standby login'); + +# Finally exercise the primary-side cleanup that the standby is meant +# to defer to. Opening a fresh session against regress_login_evt on +# the primary enters EventTriggerOnLogin()'s cleanup branch with the +# trigger list empty; AccessExclusiveLock is allowed outside recovery, +# so the flag is cleared in place. The in-place update emits a +# XLOG_HEAP_INPLACE record but does not assign an xid or write a +# commit record, so the WAL is not auto-flushed -- force a flush via +# pg_switch_wal() so the record reaches the standby. +$primary->safe_psql('regress_login_evt', 'SELECT 1'); +is( $primary->safe_psql( + 'postgres', + "SELECT dathasloginevt FROM pg_database WHERE datname = 'regress_login_evt'" + ), + 'f', + 'primary clears dathasloginevt on next login after DROP'); + +$primary->safe_psql('postgres', 'SELECT pg_switch_wal()'); +$primary->wait_for_replay_catchup($standby); +is( $standby->safe_psql( + 'postgres', + "SELECT dathasloginevt FROM pg_database WHERE datname = 'regress_login_evt'" + ), + 'f', + 'cleared dathasloginevt replicates to standby'); + +done_testing(); From 196b4b5ae612cc45a95fabf6248b3dfe389cf770 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 27 May 2026 10:35:18 +0900 Subject: [PATCH 022/250] pg_createsubscriber: Fix cleanup of publisher-side objects after errors When pg_createsubscriber fails after creating logical replication objects, it should remove the publication and replication slot that it created on the publisher. Previously, if dropping subscriber-side objects failed, pg_createsubscriber reset its internal cleanup state too early. As a result, the exit-time cleanup could skip removing the publication or replication slot on the publisher. This could leave pg_createsubscriber-created objects behind on the publisher after a failed run. That can make a retry harder, because the leftover publication or replication slot may need to be removed manually before running pg_createsubscriber again. In the case of a replication slot, leaving it behind can also retain WAL files longer than expected. The cause of this issue was that the flags made_publication and made_replslot tracking whether pg_createsubscriber created a publication or replication slot on the primary were incorrectly reset to false when failures occurred while dropping objects on the subscriber. This commit fixes the issue by preventing those cleanup flags from being reset even when failures occurred while dropping objects on the subscriber, ensuring proper cleanup of primary objects before exit on failure. Backpatch to v17, where pg_createsubscriber was added. Author: Nisha Moond Reviewed-by: David G. Johnston Reviewed-by: Fujii Masao Reviewed-by: Peter Smith Discussion: https://postgr.es/m/CABdArM5V9QKK1PkLY9dpgAcZa3kUp84-wPqPovxvdLOri4=69w@mail.gmail.com Backpatch-through: 17 --- src/bin/pg_basebackup/pg_createsubscriber.c | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c index 51d12aa7f6f..42a073e921f 100644 --- a/src/bin/pg_basebackup/pg_createsubscriber.c +++ b/src/bin/pg_basebackup/pg_createsubscriber.c @@ -115,7 +115,7 @@ static void wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt); static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo); static void drop_publication(PGconn *conn, const char *pubname, - const char *dbname, bool *made_publication); + const char *dbname); static void check_and_drop_publications(PGconn *conn, struct LogicalRepInfo *dbinfo); static void create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo); static void set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, @@ -203,8 +203,7 @@ cleanup_objects_atexit(void) if (conn != NULL) { if (dbinfo->made_publication) - drop_publication(conn, dbinfo->pubname, dbinfo->dbname, - &dbinfo->made_publication); + drop_publication(conn, dbinfo->pubname, dbinfo->dbname); if (dbinfo->made_replslot) drop_replication_slot(conn, dbinfo, dbinfo->replslotname); disconnect_database(conn, false); @@ -1465,7 +1464,6 @@ drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo, { pg_log_error("could not drop replication slot \"%s\" in database \"%s\": %s", slot_name, dbinfo->dbname, PQresultErrorMessage(res)); - dbinfo->made_replslot = false; /* don't try again. */ } PQclear(res); @@ -1705,8 +1703,7 @@ create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo) * Drop the specified publication in the given database. */ static void -drop_publication(PGconn *conn, const char *pubname, const char *dbname, - bool *made_publication) +drop_publication(PGconn *conn, const char *pubname, const char *dbname) { PQExpBuffer str = createPQExpBuffer(); PGresult *res; @@ -1736,7 +1733,6 @@ drop_publication(PGconn *conn, const char *pubname, const char *dbname, { pg_log_error("could not drop publication \"%s\" in database \"%s\": %s", pubname, dbname, PQresultErrorMessage(res)); - *made_publication = false; /* don't try again. */ /* * Don't disconnect and exit here. This routine is used by primary @@ -1786,8 +1782,7 @@ check_and_drop_publications(PGconn *conn, struct LogicalRepInfo *dbinfo) /* Drop each publication */ for (int i = 0; i < PQntuples(res); i++) - drop_publication(conn, PQgetvalue(res, i, 0), dbinfo->dbname, - &dbinfo->made_publication); + drop_publication(conn, PQgetvalue(res, i, 0), dbinfo->dbname); PQclear(res); } @@ -1797,8 +1792,7 @@ check_and_drop_publications(PGconn *conn, struct LogicalRepInfo *dbinfo) * those to provide necessary information to the user. */ if (!drop_all_pubs || dry_run) - drop_publication(conn, dbinfo->pubname, dbinfo->dbname, - &dbinfo->made_publication); + drop_publication(conn, dbinfo->pubname, dbinfo->dbname); } /* From ae08eb1687e1def3521b3915dc31f83eb209377f Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 27 May 2026 14:48:59 +0900 Subject: [PATCH 023/250] Fix race conditions in ProcKill()'s lock-group freelist handling This commit fixes two bugs in ProcKill()'s lock-group teardown freelist publication: * a double push of the leader's PGPROC that corrupts the freelist. * a leak of the last follower's PGPROC slot. ProcKill()'s lock-group teardown had two PGPROC freelist updates scattered through the function, done under two separate freeProcsLock acquisitions: * A follower's push of the leader's PGPROC, done when a follower is the last group member exiting. * Every backend's self-push at the bottom of the function. The two freelist updates were coordinated only by inspecting proc->lockGroupLeader, which a follower could clear as a side effect of pushing the leader. This coordination was broken. For example, with two concurrent backends: * The follower clears leader->lockGroupLeader and pushes the leader's PGPROC under leader_lwlock. * The follower does not clear its own proc->lockGroupLeader, being skipped. * When the leader reaches the bottom of ProcKill(), it sees a NULL proc->lockGroupLeader (the follower cleared it) and pushes itself, causing a second dlist_push_tail() of the same node onto the same freelist. * The follower at the bottom sees its own proc->lockGroupLeader being not NULL (never cleared) and skips its own push, causing its own slot to leak. This commit refactors the freelist manipulation to be done in two distinct phases, each step using its own lock acquisition to ensure that each freelist operation happens in an isolated manner for each backend (follower or leader): - First, under a single leader_lwlock acquisition, check the state of the lock-group. Depending on if we are dealing with a follower and/or a leader, and if the leader has exited before a follower, then set some state booleans that define which actions should be taken with the freelist. - Second, under a single freeProcsLock acquisition, perform the cleanup actions, self-push of a backend and/or push of the leader back to the freelist. This is an old issue, dating back to 9.6 where parallel workers and lock grouping has been added. Author: Vlad Lesin Reviewed-by: Andrey Borodin Reviewed-by: Michael Paquier Discussion: https://postgr.es/m/d2983796-2603-41b7-a66e-fc8489ddb954@gmail.com Backpatch-through: 14 --- src/backend/storage/lmgr/proc.c | 77 ++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 8f15d0c9b9e..bc8d50756ce 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -919,7 +919,10 @@ static void ProcKill(int code, Datum arg) { PGPROC *proc; + PGPROC *leader; dlist_head *procgloballist; + bool push_leader; + bool push_self; Assert(MyProc != NULL); @@ -950,35 +953,59 @@ ProcKill(int code, Datum arg) /* Cancel any pending condition variable sleep, too */ ConditionVariableCancelSleep(); + proc = MyProc; + procgloballist = proc->procgloballist; + /* - * Detach from any lock group of which we are a member. If the leader - * exits before all other group members, its PGPROC will remain allocated - * until the last group process exits; that process must return the - * leader's PGPROC to the appropriate list. + * Detach from any lock group of which we are a member, deciding under + * leader_lwlock whether we (via push_self) and/or the leader (via + * push_leader) need to be pushed onto a freelist. The actual pushes + * happen after evaluating if any of these are required, under a single + * ProcGlobal->freeProcsLock. + * + * The decision whether any of the freelists needs to be updated is taken + * under a single leader_lwlock. */ - if (MyProc->lockGroupLeader != NULL) + push_leader = false; + push_self = true; + leader = NULL; + + if (proc->lockGroupLeader != NULL) { - PGPROC *leader = MyProc->lockGroupLeader; - LWLock *leader_lwlock = LockHashPartitionLockByProc(leader); + LWLock *leader_lwlock; + + leader = proc->lockGroupLeader; + leader_lwlock = LockHashPartitionLockByProc(leader); LWLockAcquire(leader_lwlock, LW_EXCLUSIVE); Assert(!dlist_is_empty(&leader->lockGroupMembers)); - dlist_delete(&MyProc->lockGroupLink); + dlist_delete(&proc->lockGroupLink); if (dlist_is_empty(&leader->lockGroupMembers)) { leader->lockGroupLeader = NULL; - if (leader != MyProc) + if (leader != proc) { - procgloballist = leader->procgloballist; - - /* Leader exited first; return its PGPROC. */ - SpinLockAcquire(ProcStructLock); - dlist_push_head(procgloballist, &leader->links); - SpinLockRelease(ProcStructLock); + /* + * We are the last follower and the leader exited earlier; its + * PGPROC is still allocated and must be pushed here. + */ + push_leader = true; + proc->lockGroupLeader = NULL; } } - else if (leader != MyProc) - MyProc->lockGroupLeader = NULL; + else if (leader != proc) + { + /* Non-last follower; leader still present in the group. */ + proc->lockGroupLeader = NULL; + } + else + { + /* + * We are the leader and followers remain. Skip our own push; the + * last follower to exit will push us back to the freelist. + */ + push_self = false; + } LWLockRelease(leader_lwlock); } @@ -994,7 +1021,6 @@ ProcKill(int code, Datum arg) SwitchBackToLocalLatch(); pgstat_reset_wait_event_storage(); - proc = MyProc; MyProc = NULL; MyProcNumber = INVALID_PROC_NUMBER; DisownLatch(&proc->procLatch); @@ -1004,16 +1030,15 @@ ProcKill(int code, Datum arg) proc->vxid.procNumber = INVALID_PROC_NUMBER; proc->vxid.lxid = InvalidTransactionId; - procgloballist = proc->procgloballist; SpinLockAcquire(ProcStructLock); - - /* - * If we're still a member of a locking group, that means we're a leader - * which has somehow exited before its children. The last remaining child - * will release our PGPROC. Otherwise, release it now. - */ - if (proc->lockGroupLeader == NULL) + if (push_leader) + { + /* Return leader PGPROC (and semaphore) to appropriate freelist */ + dlist_push_head(leader->procgloballist, &leader->links); + } + if (push_self) { + Assert(proc->lockGroupLeader == NULL); /* Since lockGroupLeader is NULL, lockGroupMembers should be empty. */ Assert(dlist_is_empty(&proc->lockGroupMembers)); From 12c9b8b422e27bc1fc1475379b29c177cc988589 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 27 May 2026 17:19:53 +0900 Subject: [PATCH 024/250] Fix procLatch ownership race in ProcKill() DisownLatch() was executed after the PGPROC entry of the process terminated is pushed back into a freelist. A newly-forked backend that recycles the slot could call OwnLatch() and PANIC with a "latch already owned by PID", taking down the server. There were two scenarios related to lock groups where this issue could be reached: * A follower pushes the leader's PGPROC back to the freelist while the leader has not yet called DisownLatch() in its own ProcKill(). * A leader outliving all its followers pushes its own PGPROC onto the freelist before reaching DisownLatch(), which would be the most common scenario. This issue is fixed by calling SwitchBackToLocalLatch() and DisownLatch() at an earlier phase of ProcKill(), before any freelist manipulation happens, so that the slot of the backend terminated is never exposed as owning a latch. Note that pgstat_reset_wait_event_storage() is kept at a later stage. An upcoming commit will take advantage of that by introducing a test able to check the original PANIC scenario. Author: Vlad Lesin Reviewed-by: Andrey Borodin Reviewed-by: Michael Paquier Discussion: https://postgr.es/m/d2983796-2603-41b7-a66e-fc8489ddb954@gmail.com Backpatch-through: 14 --- src/backend/storage/lmgr/proc.c | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index bc8d50756ce..b59e38adf87 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -953,6 +953,24 @@ ProcKill(int code, Datum arg) /* Cancel any pending condition variable sleep, too */ ConditionVariableCancelSleep(); + /* + * Reset MyLatch to the process local one and disown the shared latch, so + * that signal handlers et al can continue using the latch after the + * shared latch isn't ours anymore. + * + * DisownLatch() must happen before our PGPROC can appear on a freelist: a + * newly-forked backend that pops our slot and calls OwnLatch() would + * PANIC on a still-owned latch. + * + * pgstat_reset_wait_event_storage() is intentionally deferred until after + * the lock-group block so that wait_event_info remains visible in our + * PGPROC slot while we may be observed there. It is safe to defer + * because our slot is not yet on any freelist at this point, and useful + * for testing purposes. + */ + SwitchBackToLocalLatch(); + DisownLatch(&MyProc->procLatch); + proc = MyProc; procgloballist = proc->procgloballist; @@ -1009,21 +1027,11 @@ ProcKill(int code, Datum arg) LWLockRelease(leader_lwlock); } - /* - * Reset MyLatch to the process local one. This is so that signal - * handlers et al can continue using the latch after the shared latch - * isn't ours anymore. - * - * Similarly, stop reporting wait events to MyProc->wait_event_info. - * - * After that clear MyProc and disown the shared latch. - */ - SwitchBackToLocalLatch(); + /* See comment above, close to DisownLatch() */ pgstat_reset_wait_event_storage(); MyProc = NULL; MyProcNumber = INVALID_PROC_NUMBER; - DisownLatch(&proc->procLatch); /* Mark the proc no longer in use */ proc->pid = 0; From f9d5a52da4ca71d592e9cef55ed676136362b8b5 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 27 May 2026 18:35:55 +0300 Subject: [PATCH 025/250] Don't try to record dependency on a dropped column's datatype When creating a relation with a dropped column, we called recordDependencyOn() also on the datatype of the dropped column, which is always InvalidOid. In versions 15 and above, that was harmless because recordDependencyOn() considers InvalidOid as a pinned object, and skips over it. On version 14, isPinnedObject() does not consider InvalidOid as pinned, so we created a bogus pg_depend entry with refobjectid == 0. As far as I can tell, the only case when AddNewAttributeTuples() is called with dropped columns is when performing a table-rewriting ALTER TABLE command. That temporarily creates a new relation with the same columns, including dropped ones, then swaps the relations, and drops the newly created table again. So even on version 14, the bogus pg_depend entry was only on the transient relation that was dropped at the end of the ALTER TABLE command, which was harmless. Even though this is harmless, let's be tidy, similar to commit 713bce9484. The reason I noticed this now and why I backported this, is because the next commit will add code to acquire locks on the referenced objects, and we don't want to acquire a lock on InvalidOid. Discussion: https://postgr.es/m/ZiYjn0eVc7pxVY45@ip-10-97-1-34.eu-west-3.compute.internal Backpatch-through: 14 --- src/backend/catalog/heap.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 72ab7df21bf..5df95ddde61 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -869,6 +869,9 @@ AddNewAttributeTuples(Oid new_rel_oid, { Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + if (attr->attisdropped) + continue; + /* Add dependency info */ ObjectAddressSubSet(myself, RelationRelationId, new_rel_oid, i + 1); ObjectAddressSet(referenced, TypeRelationId, attr->atttypid); From c8cd3d6976f7af2eceff8421ec2fd0d2bdc8dc84 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 27 May 2026 18:35:58 +0300 Subject: [PATCH 026/250] Avoid orphaned objects dependencies Concurrent DDL can leave behind objects referencing other objects that no longer exist. This can happen if an object is dropped, while a new object that depends on it is created concurrently. For example: session 1: BEGIN; CREATE FUNCTION myschema.myfunc() ...; session 2: DROP SCHEMA myschema; session 1: COMMIT; DROP SCHEMA does check that there are no objects dependending on the schema being dropped, but it does not see objects being concurrently created by other sessions. Even if it did, this scenario would still fail: session 1: BEGIN: DROP SCHEMA myschema; session 2: CREATE FUNCTION myschema.myfunc() ...; session 1: COMMIT; When the DROP SCHEMA runs, the schema was empty, but the new function is created in it before the dropping transaction completes. The CREATE FUNCTION does not see that the schema is concurrently being dropped. In both of these scenarios, the function is left behind in the schema that no longer exists. To fix, acquire AccessShareLock on all referenced objects when recording dependencies. This conflicts with the AccessExclusiveLock taken by DROP, preventing the race. After acquiring the lock, verify that the object still exists, and if it was dropped concurrently, report an error. We already had such a mechanism for shared dependencies, but for some reason we didn't do it for in-database dependendies. Ideally the locks would be acquired much earlier when creating a new object, but that will require modifying a lot of callers. This check while recording the dependency is a nice wholesale protection, and even if we change all the CREATE commands to acquire locks earlier, it's still good to have this as a backstop to catch any cases where we forgot to do so. The patch adds a few tests for some cases that left behind orphaned objects before this. It also adds a test for roles, which already had such protection, although that test is partially disabled because the error message includes an OID which is not predictable. Author: Bertrand Drouvot Reviewed-by: Heikki Linnakangas Discussion: https://postgr.es/m/ZiYjn0eVc7pxVY45@ip-10-97-1-34.eu-west-3.compute.internal Backpatch-through: 14 --- src/backend/catalog/pg_depend.c | 131 +++++++++++++++++ .../expected/ddl-dependency-locking.out | 137 ++++++++++++++++++ src/test/isolation/isolation_schedule | 1 + .../specs/ddl-dependency-locking.spec | 104 +++++++++++++ src/test/regress/expected/alter_table.out | 11 +- 5 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 src/test/isolation/expected/ddl-dependency-locking.out create mode 100644 src/test/isolation/specs/ddl-dependency-locking.spec diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 70635d9f81a..ad5ba9f9088 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -27,13 +27,17 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" static bool isObjectPinned(const ObjectAddress *object); +static void dependencyLockAndCheckObject(Oid classId, Oid objectId); /* @@ -109,6 +113,13 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; + /* + * Make sure the new referenced object doesn't go away while we record + * the dependency. DROP routines should lock the object exclusively + * before they check dependencies. + */ + dependencyLockAndCheckObject(referenced->classId, referenced->objectId); + if (slot_init_count < max_slots) { slot[slot_stored_count] = MakeSingleTupleTableSlot(RelationGetDescr(dependDesc), @@ -507,6 +518,13 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } + /* + * Make sure the new referenced object doesn't go away while we record the + * dependency. + */ + if (!newIsPinned) + dependencyLockAndCheckObject(refClassId, newRefObjectId); + depRel = table_open(DependRelationId, RowExclusiveLock); /* There should be existing dependency record(s), so search. */ @@ -714,6 +732,119 @@ isObjectPinned(const ObjectAddress *object) } +/* + * dependencyLockAndCheckObject + * + * Lock the object that we are about to record a dependency on. After it's + * locked, verify that it hasn't been dropped while we weren't looking. If it + * has been dropped, throw an an error. + * + * If the caller already holds a lock that conflicts with DROP + * (AccessShareLock or stronger), this does nothing. Callers should acquire + * locks already when they look up the dependent objects, but many callers + * currently do not. This is a backstop to make sure that we don't record a + * bogus reference permanently in the catalogs in that case. In the future, + * after we have tightened up all the callers to acquire locks earlier, this + * could just verify that the object is already locked and throw an error if + * not. + */ +static void +dependencyLockAndCheckObject(Oid classId, Oid objectId) +{ + /* + * Pinned objects cannot be dropped concurrently, and callers checked this + * already. + */ + Assert(!IsPinnedObject(classId, objectId)); + + if (classId != RelationRelationId) + { + LOCKTAG tag; + int cache; + Relation rel; + SysScanDesc scan; + ScanKeyData skey; + HeapTuple tuple; + + SET_LOCKTAG_OBJECT(tag, + MyDatabaseId, + classId, + objectId, + 0); + + if (LockHeldByMe(&tag, AccessShareLock, true)) + return; + + /* Assume we should lock the whole object not a sub-object */ + LockDatabaseObject(classId, objectId, 0, AccessShareLock); + + /* + * Check that the object still exists. If the catalog has a suitable + * syscache, check that first. + */ + cache = get_object_catcache_oid(classId); + if (cache != -1) + { + if (SearchSysCacheExists1(cache, ObjectIdGetDatum(objectId))) + return; + } + + /* + * If it's not found in the syscache, or there's no suitable syscache + * we can use, scan the catalog table using SnapshotSelf. This + * handles the case that it's an object we just created (for example, + * if it's a composite type created as part of creating a table). + */ + rel = table_open(classId, AccessShareLock); + + ScanKeyInit(&skey, + get_object_attnum_oid(classId), + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(objectId)); + + scan = systable_beginscan(rel, get_object_oid_index(classId), + true, SnapshotSelf, 1, &skey); + + tuple = systable_getnext(scan); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("dependent %s was concurrently dropped", + get_object_class_descr(classId)))); + + systable_endscan(scan); + table_close(rel, AccessShareLock); + } + else + { + /* + * Same logic for pg_class entries, but locking relations is handled + * by different functions. + * + * Callers are more careful with locking relations than other objects, + * so we should already have a lock on the relation, or on another + * object that indirectly prevents the relation from being dropped. + * For example, we might have a strong lock on a table while adding + * dependency to its index. However, we cannot detect the indirectly + * protected case here easily. To err on the safe side, acquire a + * lock directly on the relation if we're not holding one already. + */ + + /* all shared relations are pinned */ + Assert(!IsSharedRelation(objectId)); + + if (CheckRelationOidLockedByMe(objectId, AccessShareLock, true)) + return; + LockRelationOid(objectId, AccessShareLock); + + if (SearchSysCacheExists1(RELOID, ObjectIdGetDatum(objectId))) + return; + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("dependent relation was concurrently dropped"))); + } +} + /* * Various special-purpose lookups and manipulations of pg_depend. */ diff --git a/src/test/isolation/expected/ddl-dependency-locking.out b/src/test/isolation/expected/ddl-dependency-locking.out new file mode 100644 index 00000000000..3bf9e435725 --- /dev/null +++ b/src/test/isolation/expected/ddl-dependency-locking.out @@ -0,0 +1,137 @@ +Parsed test spec with 2 sessions + +starting permutation: s1_begin s1_create_function_in_schema s2_drop_schema s1_commit +step s1_begin: BEGIN; +step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; +step s2_drop_schema: DROP SCHEMA testschema; +step s1_commit: COMMIT; +step s2_drop_schema: <... completed> +ERROR: cannot drop schema testschema because other objects depend on it + +starting permutation: s2_begin s2_drop_schema s1_create_function_in_schema s2_commit +step s2_begin: BEGIN; +step s2_drop_schema: DROP SCHEMA testschema; +step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; +step s2_commit: COMMIT; +step s1_create_function_in_schema: <... completed> +ERROR: dependent schema was concurrently dropped + +starting permutation: s1_begin s1_alter_function_schema s2_drop_alterschema s1_commit +step s1_begin: BEGIN; +step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema; +step s2_drop_alterschema: DROP SCHEMA alterschema; +step s1_commit: COMMIT; +step s2_drop_alterschema: <... completed> +ERROR: cannot drop schema alterschema because other objects depend on it + +starting permutation: s2_begin s2_drop_alterschema s1_alter_function_schema s2_commit +step s2_begin: BEGIN; +step s2_drop_alterschema: DROP SCHEMA alterschema; +step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema; +step s2_commit: COMMIT; +step s1_alter_function_schema: <... completed> +ERROR: dependent schema was concurrently dropped + +starting permutation: s1_begin s1_create_function_with_argtype s2_drop_foo_type s1_commit +step s1_begin: BEGIN; +step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; +step s2_drop_foo_type: DROP TYPE public.foo; +step s1_commit: COMMIT; +step s2_drop_foo_type: <... completed> +ERROR: cannot drop type foo because other objects depend on it + +starting permutation: s2_begin s2_drop_foo_type s1_create_function_with_argtype s2_commit +step s2_begin: BEGIN; +step s2_drop_foo_type: DROP TYPE public.foo; +step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; +step s2_commit: COMMIT; +step s1_create_function_with_argtype: <... completed> +ERROR: dependent type was concurrently dropped + +starting permutation: s1_begin s1_create_function_with_rettype s2_drop_foo_rettype s1_commit +step s1_begin: BEGIN; +step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; +step s2_drop_foo_rettype: DROP DOMAIN id; +step s1_commit: COMMIT; +step s2_drop_foo_rettype: <... completed> +ERROR: cannot drop type id because other objects depend on it + +starting permutation: s2_begin s2_drop_foo_rettype s1_create_function_with_rettype s2_commit +step s2_begin: BEGIN; +step s2_drop_foo_rettype: DROP DOMAIN id; +step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; +step s2_commit: COMMIT; +step s1_create_function_with_rettype: <... completed> +ERROR: dependent type was concurrently dropped + +starting permutation: s1_begin s1_create_function_with_function s2_drop_function_f s1_commit +step s1_begin: BEGIN; +step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; +step s2_drop_function_f: DROP FUNCTION f(); +step s1_commit: COMMIT; +step s2_drop_function_f: <... completed> +ERROR: cannot drop function f() because other objects depend on it + +starting permutation: s2_begin s2_drop_function_f s1_create_function_with_function s2_commit +step s2_begin: BEGIN; +step s2_drop_function_f: DROP FUNCTION f(); +step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; +step s2_commit: COMMIT; +step s1_create_function_with_function: <... completed> +ERROR: dependent function was concurrently dropped + +starting permutation: s1_begin s1_create_domain_with_domain s2_drop_domain_id s1_commit +step s1_begin: BEGIN; +step s1_create_domain_with_domain: CREATE DOMAIN idid as id; +step s2_drop_domain_id: DROP DOMAIN id; +step s1_commit: COMMIT; +step s2_drop_domain_id: <... completed> +ERROR: cannot drop type id because other objects depend on it + +starting permutation: s2_begin s2_drop_domain_id s1_create_domain_with_domain s2_commit +step s2_begin: BEGIN; +step s2_drop_domain_id: DROP DOMAIN id; +step s1_create_domain_with_domain: CREATE DOMAIN idid as id; +step s2_commit: COMMIT; +step s1_create_domain_with_domain: <... completed> +ERROR: dependent type was concurrently dropped + +starting permutation: s1_begin s1_create_table_with_type s2_drop_footab_type s1_commit +step s1_begin: BEGIN; +step s1_create_table_with_type: CREATE TABLE tabtype(a footab); +step s2_drop_footab_type: DROP TYPE public.footab; +step s1_commit: COMMIT; +step s2_drop_footab_type: <... completed> +ERROR: cannot drop type footab because other objects depend on it + +starting permutation: s2_begin s2_drop_footab_type s1_create_table_with_type s2_commit +step s2_begin: BEGIN; +step s2_drop_footab_type: DROP TYPE public.footab; +step s1_create_table_with_type: CREATE TABLE tabtype(a footab); +step s2_commit: COMMIT; +step s1_create_table_with_type: <... completed> +ERROR: dependent type was concurrently dropped + +starting permutation: s1_begin s1_create_server_with_fdw_wrapper s2_drop_fdw_wrapper s1_commit +step s1_begin: BEGIN; +step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; +step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; +step s1_commit: COMMIT; +step s2_drop_fdw_wrapper: <... completed> +ERROR: cannot drop foreign-data wrapper fdw_wrapper because other objects depend on it + +starting permutation: s2_begin s2_drop_fdw_wrapper s1_create_server_with_fdw_wrapper s2_commit +step s2_begin: BEGIN; +step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; +step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; +step s2_commit: COMMIT; +step s1_create_server_with_fdw_wrapper: <... completed> +ERROR: dependent foreign-data wrapper was concurrently dropped + +starting permutation: s1_begin s1_alter_function_owner s2_drop_role s1_commit +step s1_begin: BEGIN; +step s1_alter_function_owner: ALTER FUNCTION public.falter() OWNER TO regress_dependency; +step s2_drop_role: DROP ROLE regress_dependency; +step s1_commit: COMMIT; +step s2_drop_role: <... completed> +ERROR: role "regress_dependency" cannot be dropped because some objects depend on it diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index abac824ec9e..204b9e399c6 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -117,3 +117,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: ddl-dependency-locking diff --git a/src/test/isolation/specs/ddl-dependency-locking.spec b/src/test/isolation/specs/ddl-dependency-locking.spec new file mode 100644 index 00000000000..de5bd88d35e --- /dev/null +++ b/src/test/isolation/specs/ddl-dependency-locking.spec @@ -0,0 +1,104 @@ +# Test that concurrent DROP and CREATE commands do not leave behind +# references to non-existent objects. + +setup +{ + CREATE SCHEMA testschema; + CREATE SCHEMA alterschema; + CREATE TYPE public.foo as enum ('one', 'two'); + CREATE TYPE public.footab as enum ('three', 'four'); + CREATE DOMAIN id AS int; + CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1; + CREATE FUNCTION public.falter() RETURNS int LANGUAGE SQL RETURN 1; + CREATE FOREIGN DATA WRAPPER fdw_wrapper; + CREATE ROLE regress_dependency; +} + +teardown +{ + DROP FUNCTION IF EXISTS testschema.foo(); + DROP FUNCTION IF EXISTS fooargtype(num foo); + DROP FUNCTION IF EXISTS footrettype(); + DROP FUNCTION IF EXISTS foofunc(); + DROP FUNCTION IF EXISTS public.falter(); + DROP FUNCTION IF EXISTS alterschema.falter(); + DROP DOMAIN IF EXISTS idid; + DROP SERVER IF EXISTS srv_fdw_wrapper; + DROP TABLE IF EXISTS tabtype; + DROP SCHEMA IF EXISTS testschema; + DROP SCHEMA IF EXISTS alterschema; + DROP TYPE IF EXISTS public.foo; + DROP TYPE IF EXISTS public.footab; + DROP DOMAIN IF EXISTS id; + DROP FUNCTION IF EXISTS f(); + DROP FOREIGN DATA WRAPPER IF EXISTS fdw_wrapper; + DROP ROLE regress_dependency; +} + +session "s1" + +step "s1_begin" { BEGIN; } +step "s1_create_function_in_schema" { CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; } +step "s1_create_function_with_argtype" { CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; } +step "s1_create_function_with_rettype" { CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; } +step "s1_create_function_with_function" { CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; } +step "s1_alter_function_owner" { ALTER FUNCTION public.falter() OWNER TO regress_dependency; } +step "s1_alter_function_schema" { ALTER FUNCTION public.falter() SET SCHEMA alterschema; } +step "s1_create_domain_with_domain" { CREATE DOMAIN idid as id; } +step "s1_create_table_with_type" { CREATE TABLE tabtype(a footab); } +step "s1_create_server_with_fdw_wrapper" { CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; } +step "s1_commit" { COMMIT; } + +session "s2" + +step "s2_begin" { BEGIN; } +step "s2_drop_schema" { DROP SCHEMA testschema; } +step "s2_drop_alterschema" { DROP SCHEMA alterschema; } +step "s2_drop_foo_type" { DROP TYPE public.foo; } +step "s2_drop_foo_rettype" { DROP DOMAIN id; } +step "s2_drop_footab_type" { DROP TYPE public.footab; } +step "s2_drop_function_f" { DROP FUNCTION f(); } +step "s2_drop_domain_id" { DROP DOMAIN id; } +step "s2_drop_fdw_wrapper" { DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; } +step "s2_drop_role" { DROP ROLE regress_dependency; } +step "s2_commit" { COMMIT; } + +# create function - drop schema +permutation "s1_begin" "s1_create_function_in_schema" "s2_drop_schema" "s1_commit" +permutation "s2_begin" "s2_drop_schema" "s1_create_function_in_schema" "s2_commit" + +# alter function - drop schema +permutation "s1_begin" "s1_alter_function_schema" "s2_drop_alterschema" "s1_commit" +permutation "s2_begin" "s2_drop_alterschema" "s1_alter_function_schema" "s2_commit" + +# create function - drop argtype +permutation "s1_begin" "s1_create_function_with_argtype" "s2_drop_foo_type" "s1_commit" +permutation "s2_begin" "s2_drop_foo_type" "s1_create_function_with_argtype" "s2_commit" + +# create function - drop rettype +permutation "s1_begin" "s1_create_function_with_rettype" "s2_drop_foo_rettype" "s1_commit" +permutation "s2_begin" "s2_drop_foo_rettype" "s1_create_function_with_rettype" "s2_commit" + +# create function - drop function used in its body +permutation "s1_begin" "s1_create_function_with_function" "s2_drop_function_f" "s1_commit" +permutation "s2_begin" "s2_drop_function_f" "s1_create_function_with_function" "s2_commit" + +# create domain over domain - drop the base domain +permutation "s1_begin" "s1_create_domain_with_domain" "s2_drop_domain_id" "s1_commit" +permutation "s2_begin" "s2_drop_domain_id" "s1_create_domain_with_domain" "s2_commit" + +# create table - drop type used in column +permutation "s1_begin" "s1_create_table_with_type" "s2_drop_footab_type" "s1_commit" +permutation "s2_begin" "s2_drop_footab_type" "s1_create_table_with_type" "s2_commit" + +# create server - drop foreign data wrapper +permutation "s1_begin" "s1_create_server_with_fdw_wrapper" "s2_drop_fdw_wrapper" "s1_commit" +permutation "s2_begin" "s2_drop_fdw_wrapper" "s1_create_server_with_fdw_wrapper" "s2_commit" + +# create function - drop owner role +permutation "s1_begin" "s1_alter_function_owner" "s2_drop_role" "s1_commit" + +# XXX: This permutation is disabled because the error message, "role +# was concurrently dropped", contains an OID that is not stable. +# +# permutation "s2_begin" "s2_drop_role" "s1_alter_function_owner" "s2_commit" diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 65a18ca1d9b..09ec96af340 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -2933,11 +2933,12 @@ begin; alter table alterlock2 add constraint alterlock2nv foreign key (f1) references alterlock (f1) NOT VALID; select * from my_locks order by 1; - relname | max_lockmode -------------+----------------------- - alterlock | ShareRowExclusiveLock - alterlock2 | ShareRowExclusiveLock -(2 rows) + relname | max_lockmode +----------------+----------------------- + alterlock | ShareRowExclusiveLock + alterlock2 | ShareRowExclusiveLock + alterlock_pkey | AccessShareLock +(3 rows) commit; begin; From 1a9b1cc18e068e181f85ab8712ac4d2274d609ab Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Wed, 27 May 2026 16:25:59 -0700 Subject: [PATCH 027/250] Fix race between ProcSignalInit() and EmitProcSignalBarrier(). Previously, ProcSignalInit() read the global barrier generation before publishing its PID into pss_pid. This created a race condition: a process could initialize its local generation with an older global value, while a concurrent EmitProcSignalBarrier() might skip that process because its pss_pid was still zero. This resulted in WaitForProcSignalBarrier() hanging indefinitely. Fix this by publishing pss_pid before reading psh_barrierGeneration with a memory barrier so that the store to pss_pid is ordered before the load. A concurrent EmitProcSignalBarrier() then either observes the published PID and signals this slot, or completes its generation increment before we load it. While this race has become more visible due to recent features using signal barriers in more places (such as online wal_level changes), the issue is theoretically present since signal barriers were introduced to release smgr caches (e.g., in DROP DATABASE). v14 has the procsiangl barrier infrastricutre but no in-tree caller that actually emits a barrier, so the case is unreachable there. This issue was also reported by buildfarm member flaviventris. Reported-by: Melanie Plageman Reviewed-by: Alexander Lakhin Reviewed-by: Matthias van de Meent Discussion: https://postgr.es/m/CAEze2WgAJmWReDN7Chtba8Er2YBvKCoa0KVN25-1evnTrHsLyA@mail.gmail.com Backpatch-through: 15 --- src/backend/storage/ipc/procsignal.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 05d99b452c3..e7c9da2b940 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -185,6 +185,15 @@ ProcSignalInit(const uint8 *cancel_key, int cancel_key_len) /* Clear out any leftover signal reasons */ MemSet(slot->pss_signalFlags, 0, NUM_PROCSIGNALS * sizeof(sig_atomic_t)); + /* + * Publish the PID before reading the global barrier generation to ensure + * that EmitProcSignalBarrier() doesn't skip us while we are grabbing an + * older generation. We need a memory barrier here to make sure that the + * update of pss_pid is ordered before the subsequent load of + * psh_barrierGeneration. + */ + pg_atomic_write_membarrier_u32(&slot->pss_pid, MyProcPid); + /* * Initialize barrier state. Since we're a brand-new process, there * shouldn't be any leftover backend-private state that needs to be @@ -204,7 +213,6 @@ ProcSignalInit(const uint8 *cancel_key, int cancel_key_len) if (cancel_key_len > 0) memcpy(slot->pss_cancel_key, cancel_key, cancel_key_len); slot->pss_cancel_key_len = cancel_key_len; - pg_atomic_write_u32(&slot->pss_pid, MyProcPid); SpinLockRelease(&slot->pss_mutex); From e5d019fbdc12281f666a94d3d2cf8ad33ae2006a Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 28 May 2026 20:58:45 +0900 Subject: [PATCH 028/250] postgres_fdw, dblink: Validate use_scram_passthrough values The use_scram_passthrough option in postgres_fdw and dblink accepts only boolean values. However, unlike other boolean options such as keep_connections, its value was not previously validated. As a result, commands such as "CREATE SERVER ... OPTIONS (use_scram_passthrough 'invalid')" could succeed unexpectedly. This commit updates postgres_fdw and dblink to validate that use_scram_passthrough is assigned a valid boolean value, and throw an error for invalid input. Backpatch to v18, where use_scram_passthrough was introduced. Author: Fujii Masao Reviewed-by: Ayush Tiwari Reviewed-by: Matheus Alcantara Discussion: https://postgr.es/m/CAHGQGwF+-k-Ehsu5W94ZP7GxS3wiBd+mi0PfGTdJ_i2Yr0zR3g@mail.gmail.com Backpatch-through: 18 --- contrib/dblink/dblink.c | 3 +++ contrib/postgres_fdw/option.c | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c index 2f803e7ed3c..62706dae004 100644 --- a/contrib/dblink/dblink.c +++ b/contrib/dblink/dblink.c @@ -2010,6 +2010,9 @@ dblink_fdw_validator(PG_FUNCTION_ARGS) closest_match) : 0 : errhint("There are no valid options in this context."))); } + + if (strcmp(def->defname, "use_scram_passthrough") == 0) + (void) defGetBoolean(def); /* accept only boolean values */ } PG_RETURN_VOID(); diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c index 6d5795b9fa9..0ab1fd70186 100644 --- a/contrib/postgres_fdw/option.c +++ b/contrib/postgres_fdw/option.c @@ -125,7 +125,8 @@ postgres_fdw_validator(PG_FUNCTION_ARGS) strcmp(def->defname, "async_capable") == 0 || strcmp(def->defname, "parallel_commit") == 0 || strcmp(def->defname, "parallel_abort") == 0 || - strcmp(def->defname, "keep_connections") == 0) + strcmp(def->defname, "keep_connections") == 0 || + strcmp(def->defname, "use_scram_passthrough") == 0) { /* these accept only boolean values */ (void) defGetBoolean(def); From c0bf1d89df29e81c6fdad64e0f7cde10f16322bd Mon Sep 17 00:00:00 2001 From: Andres Freund Date: Thu, 28 May 2026 11:34:11 -0400 Subject: [PATCH 029/250] Make stack depth check work with asan's use-after-return With address sanitizer's stack-use-after-return check, stack variables are moved to heap allocations, to allow to detect references to the memory at a later time. That broke our stack-depth check, which is why we had to disable detect_stack_use_after_return in CI. Luckily __builtin_frame_address() works correctly, even under asan, so use that. We started using __builtin_frame_address() with de447bb8e6fb, however as of that commit we just used it for the stack base address, not for the value to compare to the base address. Now we use it for both. When building without __builtin_frame_address() support, we continue to use stack variables for the stack depth determination. Reviewed-by: Tom Lane Discussion: https://postgr.es/m/2kk4z4odvuyrg7qlwjd7ft4eron4cle4btb33v4qatgsdkayir@gj6e62rgsel4 Backpatch-through: 14 --- .cirrus.tasks.yml | 2 +- src/backend/utils/misc/stack_depth.c | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.cirrus.tasks.yml b/.cirrus.tasks.yml index 1f32c53bf83..09ee1a47674 100644 --- a/.cirrus.tasks.yml +++ b/.cirrus.tasks.yml @@ -406,7 +406,7 @@ task: # print_stacktraces=1,verbosity=2, duh # detect_leaks=0: too many uninteresting leak errors in short-lived binaries UBSAN_OPTIONS: print_stacktrace=1:disable_coredump=0:abort_on_error=1:verbosity=2 - ASAN_OPTIONS: print_stacktrace=1:disable_coredump=0:abort_on_error=1:detect_leaks=0:detect_stack_use_after_return=0 + ASAN_OPTIONS: print_stacktrace=1:disable_coredump=0:abort_on_error=1:detect_leaks=0 # SANITIZER_FLAGS is set in the tasks below CFLAGS: -Og -ggdb -fno-sanitize-recover=all $SANITIZER_FLAGS diff --git a/src/backend/utils/misc/stack_depth.c b/src/backend/utils/misc/stack_depth.c index 8f7cf531fbc..2b976731880 100644 --- a/src/backend/utils/misc/stack_depth.c +++ b/src/backend/utils/misc/stack_depth.c @@ -53,7 +53,8 @@ set_stack_base(void) /* * Set up reference point for stack depth checking. On recent gcc we use * __builtin_frame_address() to avoid a warning about storing a local - * variable's address in a long-lived variable. + * variable's address in a long-lived variable. This is also important + * with address sanitizer, see comment in stack_is_too_deep(). */ #ifdef HAVE__BUILTIN_FRAME_ADDRESS stack_base_ptr = __builtin_frame_address(0); @@ -108,13 +109,28 @@ check_stack_depth(void) bool stack_is_too_deep(void) { +#ifndef HAVE__BUILTIN_FRAME_ADDRESS char stack_top_loc; +#endif ssize_t stack_depth; + char *stack_address; + + /* + * With address sanitizer's stack-use-after-return check, stack variables + * are moved to heap allocations, to allow to detect references to the + * memory at a later time. That would break our stack-depth check. Luckily + * __builtin_frame_address() works correctly, even under asan. + */ +#ifndef HAVE__BUILTIN_FRAME_ADDRESS + stack_address = &stack_top_loc; +#else + stack_address = (char *) __builtin_frame_address(0); +#endif /* - * Compute distance from reference point to my local variables + * Compute distance from reference point to my stack frame. */ - stack_depth = (ssize_t) (stack_base_ptr - &stack_top_loc); + stack_depth = (ssize_t) (stack_base_ptr - stack_address); /* * Take abs value, since stacks grow up on some machines, down on others From c8b4186d6eef1bb310708e02aa872947fba87e9f Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 28 May 2026 21:27:50 +0300 Subject: [PATCH 030/250] Use term "referenced" rather than "dependent" in dependency locking Reported-by: Kyotaro Horiguchi Discussion: https://www.postgresql.org/message-id/20260528.114608.488039299811669368.horikyota.ntt@gmail.com Backpatch-through: 14 --- src/backend/catalog/pg_depend.c | 6 +++--- .../expected/ddl-dependency-locking.out | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index ad5ba9f9088..48f0ec7ed27 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -741,7 +741,7 @@ isObjectPinned(const ObjectAddress *object) * * If the caller already holds a lock that conflicts with DROP * (AccessShareLock or stronger), this does nothing. Callers should acquire - * locks already when they look up the dependent objects, but many callers + * locks already when they look up the referenced objects, but many callers * currently do not. This is a backstop to make sure that we don't record a * bogus reference permanently in the catalogs in that case. In the future, * after we have tightened up all the callers to acquire locks earlier, this @@ -809,7 +809,7 @@ dependencyLockAndCheckObject(Oid classId, Oid objectId) if (!HeapTupleIsValid(tuple)) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("dependent %s was concurrently dropped", + errmsg("referenced %s was concurrently dropped", get_object_class_descr(classId)))); systable_endscan(scan); @@ -841,7 +841,7 @@ dependencyLockAndCheckObject(Oid classId, Oid objectId) return; ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("dependent relation was concurrently dropped"))); + errmsg("referenced relation was concurrently dropped"))); } } diff --git a/src/test/isolation/expected/ddl-dependency-locking.out b/src/test/isolation/expected/ddl-dependency-locking.out index 3bf9e435725..636de281022 100644 --- a/src/test/isolation/expected/ddl-dependency-locking.out +++ b/src/test/isolation/expected/ddl-dependency-locking.out @@ -14,7 +14,7 @@ step s2_drop_schema: DROP SCHEMA testschema; step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; step s2_commit: COMMIT; step s1_create_function_in_schema: <... completed> -ERROR: dependent schema was concurrently dropped +ERROR: referenced schema was concurrently dropped starting permutation: s1_begin s1_alter_function_schema s2_drop_alterschema s1_commit step s1_begin: BEGIN; @@ -30,7 +30,7 @@ step s2_drop_alterschema: DROP SCHEMA alterschema; step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema; step s2_commit: COMMIT; step s1_alter_function_schema: <... completed> -ERROR: dependent schema was concurrently dropped +ERROR: referenced schema was concurrently dropped starting permutation: s1_begin s1_create_function_with_argtype s2_drop_foo_type s1_commit step s1_begin: BEGIN; @@ -46,7 +46,7 @@ step s2_drop_foo_type: DROP TYPE public.foo; step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; step s2_commit: COMMIT; step s1_create_function_with_argtype: <... completed> -ERROR: dependent type was concurrently dropped +ERROR: referenced type was concurrently dropped starting permutation: s1_begin s1_create_function_with_rettype s2_drop_foo_rettype s1_commit step s1_begin: BEGIN; @@ -62,7 +62,7 @@ step s2_drop_foo_rettype: DROP DOMAIN id; step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; step s2_commit: COMMIT; step s1_create_function_with_rettype: <... completed> -ERROR: dependent type was concurrently dropped +ERROR: referenced type was concurrently dropped starting permutation: s1_begin s1_create_function_with_function s2_drop_function_f s1_commit step s1_begin: BEGIN; @@ -78,7 +78,7 @@ step s2_drop_function_f: DROP FUNCTION f(); step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; step s2_commit: COMMIT; step s1_create_function_with_function: <... completed> -ERROR: dependent function was concurrently dropped +ERROR: referenced function was concurrently dropped starting permutation: s1_begin s1_create_domain_with_domain s2_drop_domain_id s1_commit step s1_begin: BEGIN; @@ -94,7 +94,7 @@ step s2_drop_domain_id: DROP DOMAIN id; step s1_create_domain_with_domain: CREATE DOMAIN idid as id; step s2_commit: COMMIT; step s1_create_domain_with_domain: <... completed> -ERROR: dependent type was concurrently dropped +ERROR: referenced type was concurrently dropped starting permutation: s1_begin s1_create_table_with_type s2_drop_footab_type s1_commit step s1_begin: BEGIN; @@ -110,7 +110,7 @@ step s2_drop_footab_type: DROP TYPE public.footab; step s1_create_table_with_type: CREATE TABLE tabtype(a footab); step s2_commit: COMMIT; step s1_create_table_with_type: <... completed> -ERROR: dependent type was concurrently dropped +ERROR: referenced type was concurrently dropped starting permutation: s1_begin s1_create_server_with_fdw_wrapper s2_drop_fdw_wrapper s1_commit step s1_begin: BEGIN; @@ -126,7 +126,7 @@ step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; step s2_commit: COMMIT; step s1_create_server_with_fdw_wrapper: <... completed> -ERROR: dependent foreign-data wrapper was concurrently dropped +ERROR: referenced foreign-data wrapper was concurrently dropped starting permutation: s1_begin s1_alter_function_owner s2_drop_role s1_commit step s1_begin: BEGIN; From 380a8b2ea024c33a35e7abc8628e7c4f52f9f9f9 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Fri, 29 May 2026 14:39:03 -0700 Subject: [PATCH 031/250] doc: Correct the timeline for OAuth's shutdown_cb During original feature development, the OAuth validator shutdown callback was invoked via before_shmem_exit(). That was changed to use a reset callback before commit, but I forgot to update the documentation for validator developers. Correct this and backport to 18, where OAuth was introduced. The callback is invoked whenever the server is "finished" with token validation. (We make no stronger guarantees here, in the hopes that this API might successfully navigate future multifactor authentication support and/or changes to the server threading model.) Reported-by: Zsolt Parragi Reviewed-by: Zsolt Parragi Reviewed-by: Chao Li Discussion: https://postgr.es/m/CAN4CZFOuMb_gnLvCwRdMybg_k8WRNJTjcij%2BPoQkuQHDUzxGWg%40mail.gmail.com Backpatch-through: 18 --- doc/src/sgml/oauth-validators.sgml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/oauth-validators.sgml b/doc/src/sgml/oauth-validators.sgml index 704089dd7b3..3b9dad47411 100644 --- a/doc/src/sgml/oauth-validators.sgml +++ b/doc/src/sgml/oauth-validators.sgml @@ -403,9 +403,10 @@ typedef struct ValidatorModuleResult Shutdown Callback - The shutdown_cb callback is executed when the backend - process associated with the connection exits. If the validator module has - any allocated state, this callback should free it to avoid resource leaks. + The shutdown_cb callback is executed when the server + backend has finished validating tokens for the connection. If the validator + module has any allocated state, this callback should free it to avoid + resource leaks. typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state); From 1e9bc4074beb7678bc2fed1a104929f8e9da1615 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 3 Jun 2026 08:58:29 +0900 Subject: [PATCH 032/250] psql: Fix issues with deferred errors in pipelines When an error is raised while processing a Sync message in a pipeline, like a deferred constraint violation, the error was not associated with the piped command and was not counted in available_results. This caused assertion failures in discardAbortedPipelineResults(), keeping an incorrect state at pipeline exit, because the code assumed that the number of available and requested results would always be positive, expecting all the counters to be 0 at the end of a pipeline. This commit switches discardAbortedPipelineResults() and ExecQueryAndProcessResults() to take a softer approach when consuming and draining the results after an error. If there are still piped syncs in the pipeline when it ends, we now attempt to consume them before leaving the pipeline mode. Alexander has been able to reach two assertion failures through his testing. While investigating more this issue, I have bumped into two more. Most of these cases are covered by the regression tests added in this commit, plus some cases with mixes of pipelines, deferred errors and results fetched. Some of the tests discussed (like the backend termination one) could not be included in this commit but have been tested manually. Another test scenario discussed involved the injection of an error state in the backend, that was able to trick libpq internally and put its queue out of sync. This scenario is not going to happen in practice, but if we were to do something about it we would need to make libpq understand that it needs to fail in some cases but not block. Reported-by: Alexander Lakhin Author: Michael Paquier Discussion: https://postgr.es/m/19494-97a86d84fee71c47@postgresql.org Backpatch-through: 18 --- src/bin/psql/common.c | 76 +++++++++--- src/test/regress/expected/psql_pipeline.out | 124 ++++++++++++++++++++ src/test/regress/sql/psql_pipeline.sql | 63 ++++++++++ 3 files changed, 245 insertions(+), 18 deletions(-) diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index cd329ade12b..7e005db5750 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -1497,11 +1497,24 @@ discardAbortedPipelineResults(void) } else if (res == NULL) { - /* A query was processed, decrement the counters */ - Assert(pset.available_results > 0); - Assert(pset.requested_results > 0); - pset.available_results--; - pset.requested_results--; + /* + * A query was processed, decrement the counters. + * + * It is possible to get here with available_results == 0 when an + * error is generated by the Sync message processing itself. Such + * errors are not counted in available_results because they are + * not associated with a piped command. In that case, skip the + * counter decrements and continue to find the Sync result. + * + * If the connection has been lost, there will never be any more + * results to read, so bail out. + */ + if (!ConnectionUp()) + return NULL; + if (pset.available_results > 0) + pset.available_results--; + if (pset.requested_results > 0) + pset.requested_results--; } if (pset.requested_results == 0) @@ -2042,14 +2055,16 @@ ExecQueryAndProcessResults(const char *query, if (result_status == PGRES_PIPELINE_SYNC) { - Assert(pset.piped_syncs > 0); - /* * Sync response, decrease the sync and requested_results - * counters. + * counters. Guard against underflow: an error during Sync + * processing on the server can cause the client-side counter to + * drift. */ - pset.piped_syncs--; - pset.requested_results--; + if (pset.piped_syncs > 0) + pset.piped_syncs--; + if (pset.requested_results > 0) + pset.requested_results--; /* * After a synchronisation point, reset success state to print @@ -2071,8 +2086,10 @@ ExecQueryAndProcessResults(const char *query, * In a pipeline with a non-sync response? Decrease the result * counters. */ - pset.available_results--; - pset.requested_results--; + if (pset.available_results > 0) + pset.available_results--; + if (pset.requested_results > 0) + pset.requested_results--; } /* @@ -2173,14 +2190,37 @@ ExecQueryAndProcessResults(const char *query, if (end_pipeline) { - /* after a pipeline is processed, pipeline piped_syncs should be 0 */ - Assert(pset.piped_syncs == 0); - /* all commands have been processed */ - Assert(pset.piped_commands == 0); - /* all results were read */ - Assert(pset.available_results == 0); + /* + * Reset available/requested results. Normally these are already 0, + * but an error generated by a Sync processing itself can leave some + * of them behind. Consume them before exiting pipeline mode. + */ + while (pset.piped_syncs > 0) + { + PGresult *remaining; + + remaining = PQgetResult(pset.db); + + if (remaining == NULL) + { + if (!ConnectionUp()) + break; + continue; + } + if (PQresultStatus(remaining) == PGRES_PIPELINE_SYNC) + pset.piped_syncs--; + PQclear(remaining); + } + pset.piped_syncs = 0; + pset.piped_commands = 0; + pset.available_results = 0; + pset.requested_results = 0; + + if (PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF) + PQexitPipelineMode(pset.db); } Assert(pset.requested_results == 0); + SetPipelineVariables(); /* may need this to recover from conn loss during COPY */ diff --git a/src/test/regress/expected/psql_pipeline.out b/src/test/regress/expected/psql_pipeline.out index a0816fb10b6..a931d63cafe 100644 --- a/src/test/regress/expected/psql_pipeline.out +++ b/src/test/regress/expected/psql_pipeline.out @@ -764,5 +764,129 @@ VACUUM psql_pipeline \bind \sendpipeline 1 (1 row) +-- Deferred constraint violation at commit time in a pipeline. +CREATE TABLE psql_pipeline_defer (a INTEGER PRIMARY KEY DEFERRABLE INITIALLY DEFERRED); +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) RETURNING * \bind 1 \sendpipeline +\endpipeline + a +--- + 1 + 1 +(2 rows) + +ERROR: duplicate key value violates unique constraint "psql_pipeline_defer_pkey" +DETAIL: Key (a)=(1) already exists. +-- Same with \syncpipeline and commands after the failing sync. +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\syncpipeline +SELECT $1 \bind 'after_sync_1' \sendpipeline +\endpipeline +ERROR: duplicate key value violates unique constraint "psql_pipeline_defer_pkey" +DETAIL: Key (a)=(1) already exists. + ?column? +-------------- + after_sync_1 +(1 row) + +-- More patterns with more \syncpipeline, more commands and \getresults +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\syncpipeline +SELECT $1 \bind 'after_sync_1' \sendpipeline +\getresults +ERROR: duplicate key value violates unique constraint "psql_pipeline_defer_pkey" +DETAIL: Key (a)=(1) already exists. +SELECT $1 \bind 'after_sync_2' \sendpipeline +\endpipeline + ?column? +-------------- + after_sync_1 +(1 row) + + ?column? +-------------- + after_sync_2 +(1 row) + +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\syncpipeline +\getresults +ERROR: duplicate key value violates unique constraint "psql_pipeline_defer_pkey" +DETAIL: Key (a)=(1) already exists. +SELECT $1 \bind 'after_sync_1' \sendpipeline +\getresults +SELECT $1 \bind 'after_sync_2' \sendpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +SELECT $1 \bind 'after_sync_3' \sendpipeline +SELECT $1 \bind 'after_sync_4' \sendpipeline +SELECT $1 \bind 'after_sync_5' \sendpipeline +\endpipeline + ?column? +-------------- + after_sync_1 +(1 row) + + ?column? +-------------- + after_sync_2 +(1 row) + + ?column? +-------------- + after_sync_3 +(1 row) + + ?column? +-------------- + after_sync_4 +(1 row) + + ?column? +-------------- + after_sync_5 +(1 row) + +ERROR: duplicate key value violates unique constraint "psql_pipeline_defer_pkey" +DETAIL: Key (a)=(1) already exists. +-- Deferred error combined with a regular command error after the sync. +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\syncpipeline +SELECT $1 \bind \sendpipeline +SELECT $1 \bind 'after_error' \sendpipeline +\endpipeline +ERROR: duplicate key value violates unique constraint "psql_pipeline_defer_pkey" +DETAIL: Key (a)=(1) already exists. +ERROR: bind message supplies 0 parameters, but prepared statement "" requires 1 +-- Empty sync segment followed by a deferred error. +\startpipeline +\syncpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\endpipeline +ERROR: duplicate key value violates unique constraint "psql_pipeline_defer_pkey" +DETAIL: Key (a)=(1) already exists. +-- Deferred error with \getresults reading results one at a time. +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +SELECT $1 \bind 'partial' \sendpipeline +\syncpipeline +\getresults 1 +\getresults 1 + ?column? +---------- + partial +(1 row) + +\getresults +ERROR: duplicate key value violates unique constraint "psql_pipeline_defer_pkey" +DETAIL: Key (a)=(1) already exists. +\endpipeline +DROP TABLE psql_pipeline_defer; -- Clean up DROP TABLE psql_pipeline; diff --git a/src/test/regress/sql/psql_pipeline.sql b/src/test/regress/sql/psql_pipeline.sql index 6788dceee2e..468ef1d090b 100644 --- a/src/test/regress/sql/psql_pipeline.sql +++ b/src/test/regress/sql/psql_pipeline.sql @@ -438,5 +438,68 @@ SELECT 1 \bind \sendpipeline VACUUM psql_pipeline \bind \sendpipeline \endpipeline +-- Deferred constraint violation at commit time in a pipeline. +CREATE TABLE psql_pipeline_defer (a INTEGER PRIMARY KEY DEFERRABLE INITIALLY DEFERRED); +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) RETURNING * \bind 1 \sendpipeline +\endpipeline + +-- Same with \syncpipeline and commands after the failing sync. +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\syncpipeline +SELECT $1 \bind 'after_sync_1' \sendpipeline +\endpipeline + +-- More patterns with more \syncpipeline, more commands and \getresults +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\syncpipeline +SELECT $1 \bind 'after_sync_1' \sendpipeline +\getresults +SELECT $1 \bind 'after_sync_2' \sendpipeline +\endpipeline +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\syncpipeline +\getresults +SELECT $1 \bind 'after_sync_1' \sendpipeline +\getresults +SELECT $1 \bind 'after_sync_2' \sendpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +SELECT $1 \bind 'after_sync_3' \sendpipeline +SELECT $1 \bind 'after_sync_4' \sendpipeline +SELECT $1 \bind 'after_sync_5' \sendpipeline +\endpipeline + +-- Deferred error combined with a regular command error after the sync. +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\syncpipeline +SELECT $1 \bind \sendpipeline +SELECT $1 \bind 'after_error' \sendpipeline +\endpipeline + +-- Empty sync segment followed by a deferred error. +\startpipeline +\syncpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +\endpipeline + +-- Deferred error with \getresults reading results one at a time. +\startpipeline +INSERT INTO psql_pipeline_defer VALUES ($1), ($1) \bind 1 \sendpipeline +SELECT $1 \bind 'partial' \sendpipeline +\syncpipeline +\getresults 1 +\getresults 1 +\getresults +\endpipeline + +DROP TABLE psql_pipeline_defer; + -- Clean up DROP TABLE psql_pipeline; From cc0819e78ae321fd01bb40751be1f765b3932aaa Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Wed, 3 Jun 2026 09:36:52 +0900 Subject: [PATCH 033/250] Fix wrong unsafe-flag test in check_output_expressions() The check for window functions (point 4) guarded on the wrong bit: it tested UNSAFE_NOTIN_DISTINCTON_CLAUSE while setting UNSAFE_NOTIN_PARTITIONBY_CLAUSE. Each check in this loop guards on the same bit it is about to set, as an idempotency optimization, since unsafeFlags[] is accumulated across the arms of a set operation and there is no point recomputing a column's status once its bit is present. This is not a live bug. When UNSAFE_NOTIN_PARTITIONBY_CLAUSE is already set but UNSAFE_NOTIN_DISTINCTON_CLAUSE is not, the guard fails to skip targetIsInAllPartitionLists() and recomputes it, but setting the same bit again changes nothing. When UNSAFE_NOTIN_DISTINCTON_CLAUSE is already set, point 4 is skipped and UNSAFE_NOTIN_PARTITIONBY_CLAUSE is left unset; but such a column is already unsafe for pushdown via UNSAFE_NOTIN_DISTINCTON_CLAUSE, so the outcome is unchanged. To fix, test UNSAFE_NOTIN_PARTITIONBY_CLAUSE, matching the bit being set and the pattern of the surrounding checks. Back-patch to v15, where the buggy check was introduced. Author: Richard Guo Reviewed-by: Tender Wang Reviewed-by: David Rowley Discussion: https://postgr.es/m/CAMbWs49Q_xnF_P2QSUyDzJ34MnrO7dh-cUAaK2HJPgSgh88NcA@mail.gmail.com Backpatch-through: 15 --- src/backend/optimizer/path/allpaths.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 8feeed9f303..53208be5107 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -3818,7 +3818,7 @@ check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo) /* If subquery uses window functions, check point 4 */ if (subquery->hasWindowFuncs && (safetyInfo->unsafeFlags[tle->resno] & - UNSAFE_NOTIN_DISTINCTON_CLAUSE) == 0 && + UNSAFE_NOTIN_PARTITIONBY_CLAUSE) == 0 && !targetIsInAllPartitionLists(tle, subquery)) { /* not present in all PARTITION BY clauses, so mark it unsafe */ From b3f13c0324d2e6540cc00076a1204719e5c91017 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 3 Jun 2026 12:47:26 +0900 Subject: [PATCH 034/250] Fix copy-paste error in hash_record_extended() The code failed to initialize the second isnull argument passed to FunctionCallInvoke(). This is harmless for existing in-core extended hash support functions, since FunctionCallInvoke() does not use the value (note that all the in-core extended hash functions are strict), examining only the argument values. However, extension-provided extended hash functions could be affected if they inspect PG_ARGISNULL(1). Oversight in 01e658fa74cb. Author: Man Zeng Discussion: https://postgr.es/m/tencent_7818173C01E01836109848C3@qq.com Backpatch-through: 14 --- src/backend/utils/adt/rowtypes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/utils/adt/rowtypes.c b/src/backend/utils/adt/rowtypes.c index fe5edc0027d..0e8305f6b7d 100644 --- a/src/backend/utils/adt/rowtypes.c +++ b/src/backend/utils/adt/rowtypes.c @@ -2030,7 +2030,7 @@ hash_record_extended(PG_FUNCTION_ARGS) locfcinfo->args[0].value = values[i]; locfcinfo->args[0].isnull = false; locfcinfo->args[1].value = Int64GetDatum(seed); - locfcinfo->args[0].isnull = false; + locfcinfo->args[1].isnull = false; element_hash = DatumGetUInt64(FunctionCallInvoke(locfcinfo)); /* We don't expect hash support functions to return null */ From f833c92077a1ba7fd8bb5d51dc466409e2fd33b0 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 3 Jun 2026 18:46:49 +0900 Subject: [PATCH 035/250] Fix race in ReplicationSlotRelease() for ephemeral slots When releasing an ephemeral replication slot, ReplicationSlotRelease() drops the slot via ReplicationSlotDropAcquired(). However, after dropping the slot, ReplicationSlotRelease() continued to use its local "slot" pointer, which still referenced the dropped slot's former shared-memory entry. It could then update fields such as effective_xmin in that entry. Once an ephemeral slot has been dropped (via ReplicationSlotDropAcquired()), its slot array entry can be reused immediately by another backend creating a new slot. As a result, those updates could corrupt the state of an unrelated replication slot. Fix by skipping those shared-memory updates for phemeral slots and performing them only for non-ephemeral slots, whose shared-memory entries remain valid after release. Backpatch to all supported versions. Author: Zhijie Hou Reviewed-by: Masao Fujii Reviewed-by: Srinath Reddy Sadipiralla Reviewed-by: Xuneng Zhou Discussion: https://postgr.es/m/TY4PR01MB177184FF9EE916F577E1F554194082@TY4PR01MB17718.jpnprd01.prod.outlook.com Backpatch-through: 14 --- src/backend/replication/slot.c | 68 +++++++++++++++++----------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index 4246d0a51e1..e60ccc424dd 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -737,44 +737,46 @@ ReplicationSlotRelease(void) */ ReplicationSlotDropAcquired(); } - - /* - * If slot needed to temporarily restrain both data and catalog xmin to - * create the catalog snapshot, remove that temporary constraint. - * Snapshots can only be exported while the initial snapshot is still - * acquired. - */ - if (!TransactionIdIsValid(slot->data.xmin) && - TransactionIdIsValid(slot->effective_xmin)) + else { - SpinLockAcquire(&slot->mutex); - slot->effective_xmin = InvalidTransactionId; - SpinLockRelease(&slot->mutex); - ReplicationSlotsComputeRequiredXmin(false); - } - - /* - * Set the time since the slot has become inactive. We get the current - * time beforehand to avoid system call while holding the spinlock. - */ - now = GetCurrentTimestamp(); + /* + * If slot needed to temporarily restrain both data and catalog xmin + * to create the catalog snapshot, remove that temporary constraint. + * Snapshots can only be exported while the initial snapshot is still + * acquired. + */ + if (!TransactionIdIsValid(slot->data.xmin) && + TransactionIdIsValid(slot->effective_xmin)) + { + SpinLockAcquire(&slot->mutex); + slot->effective_xmin = InvalidTransactionId; + SpinLockRelease(&slot->mutex); + ReplicationSlotsComputeRequiredXmin(false); + } - if (slot->data.persistency == RS_PERSISTENT) - { /* - * Mark persistent slot inactive. We're not freeing it, just - * disconnecting, but wake up others that may be waiting for it. + * Set the time since the slot has become inactive. We get the current + * time beforehand to avoid system call while holding the spinlock. */ - SpinLockAcquire(&slot->mutex); - slot->active_pid = 0; - ReplicationSlotSetInactiveSince(slot, now, false); - SpinLockRelease(&slot->mutex); - ConditionVariableBroadcast(&slot->active_cv); - } - else - ReplicationSlotSetInactiveSince(slot, now, true); + now = GetCurrentTimestamp(); - MyReplicationSlot = NULL; + if (slot->data.persistency == RS_PERSISTENT) + { + /* + * Mark persistent slot inactive. We're not freeing it, just + * disconnecting, but wake up others that may be waiting for it. + */ + SpinLockAcquire(&slot->mutex); + slot->active_pid = 0; + ReplicationSlotSetInactiveSince(slot, now, false); + SpinLockRelease(&slot->mutex); + ConditionVariableBroadcast(&slot->active_cv); + } + else + ReplicationSlotSetInactiveSince(slot, now, true); + + MyReplicationSlot = NULL; + } /* might not have been set when we've been a plain slot */ LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); From 94c02de89c2632eeb870899c89224ca1a23d2ae1 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Wed, 3 Jun 2026 11:33:35 +0300 Subject: [PATCH 036/250] pg_dump: scope indAttNames per index in getIndexes() getIndexes() declared indAttNames and nindAttNames in the outer per-table loop, so the names collected for an index on expressions were carried over to the next plain index in the same table. This is an internal inconsistency rather than a user-facing bug. dumpRelationStats_dumper() only walks indexes that have pg_statistic rows, and ANALYZE only creates those for indexes with expressions, so the second index in the affected pair is not visited and the stale array is never consulted. Fix by moving the two variables into the inner per-index loop so each iteration starts with a clean slate. Author: Maksim Melnikov Reviewed-by: Alexander Korotkov Discussion: https://postgr.es/m/be5fc489-587e-421f-bbb8-adb43cfd50f4@postgrespro.ru Backpatch-through: 17 --- src/bin/pg_dump/pg_dump.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 8f6332d731f..3ad7ec2ec48 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -7908,8 +7908,6 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) { Oid indrelid = atooid(PQgetvalue(res, j, i_indrelid)); TableInfo *tbinfo = NULL; - char **indAttNames = NULL; - int nindAttNames = 0; int numinds; /* Count rows for this table */ @@ -7943,6 +7941,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) { char contype; char indexkind; + char **indAttNames = NULL; + int nindAttNames = 0; RelStatsInfo *relstats; int32 relpages = atoi(PQgetvalue(res, j, i_relpages)); int32 relallvisible = atoi(PQgetvalue(res, j, i_relallvisible)); From 0228d098ac48b6f82781c0382fe875d559f1042a Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 4 Jun 2026 11:37:43 -0400 Subject: [PATCH 037/250] Fix another case of indirectly casting away const. Like 8f1791c61, this fixes a case of implicitly casting away const by not treating the result of strrchr() on a const pointer as const. This was missed at the time because the machines reporting those warnings weren't building with --with-llvm. While here, clean up another infelicity: in the probably- impossible case that the input string contains only one dot, this function would call pnstrdup() with a length of -1 and thereby emit a module name equal to the function name. It seems to me we should emit modname = NULL instead. Also remove a useless Assert and two redundant assignments. Back-patch, as 8f1791c61 was, so that users of back branches don't see this warning when building with late-model gcc. Reported-by: hubert depesz lubaczewski Author: Tom Lane Discussion: https://postgr.es/m/aiGNJ89PBqvq2Yyz@depesz.com Backpatch-through: 14 --- src/backend/jit/llvm/llvmjit.c | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c index e64f9b31c43..2d448f3cc87 100644 --- a/src/backend/jit/llvm/llvmjit.c +++ b/src/backend/jit/llvm/llvmjit.c @@ -1048,9 +1048,6 @@ llvm_create_types(void) void llvm_split_symbol_name(const char *name, char **modname, char **funcname) { - *modname = NULL; - *funcname = NULL; - /* * Module function names are pgextern.$module.$funcname */ @@ -1060,14 +1057,21 @@ llvm_split_symbol_name(const char *name, char **modname, char **funcname) * Symbol names cannot contain a ., therefore we can split based on * first and last occurrence of one. */ - *funcname = rindex(name, '.'); - (*funcname)++; /* jump over . */ - - *modname = pnstrdup(name + strlen("pgextern."), - *funcname - name - strlen("pgextern.") - 1); - Assert(funcname); + const char *lastdot; - *funcname = pstrdup(*funcname); + name += strlen("pgextern."); + lastdot = strrchr(name, '.'); + if (lastdot) + { + *modname = pnstrdup(name, lastdot - name); + *funcname = pstrdup(lastdot + 1); + } + else + { + /* hmm, no second dot? */ + *modname = NULL; + *funcname = pstrdup(name); + } } else { From c5194139cb4c9cf8284a6e433418c0323a7e7650 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 4 Jun 2026 12:24:51 -0400 Subject: [PATCH 038/250] Improve reporting of invalid weight symbols in setweight() et al. This commit addresses two related issues: tsvector_filter() assumed it could print an incorrect weight value with %c. This could result in an invalidly-encoded error message if the database encoding is multibyte and the char value has its high bit set. Weight values that are ASCII control characters could render illegibly too. Fix by printing such values in octal (\ooo), similarly to how charout() would render them. tsvector_setweight() and tsvector_setweight_by_filter() reported the same unrecognized-weight error condition with elog(), as though it were an internal error. That'd not translate, would produce an unwanted XX000 SQLSTATE code, and also reported the bad value as a decimal integer which seems unhelpful. Fix by refactoring so that all three functions share one copy of the code that interprets a weight argument. The invalid-encoding aspect seems to me (tgl) to justify back-patching. Author: Ewan Young Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAON2xHNaeLAUzRCXL5AmXLcXaSE_gWAVjWQRmLzc_oZ=1_Vf4Q@mail.gmail.com Backpatch-through: 14 --- src/backend/utils/adt/tsvector_op.c | 87 ++++++++++------------------- 1 file changed, 30 insertions(+), 57 deletions(-) diff --git a/src/backend/utils/adt/tsvector_op.c b/src/backend/utils/adt/tsvector_op.c index ea84e1cf123..ebe12ed5235 100644 --- a/src/backend/utils/adt/tsvector_op.c +++ b/src/backend/utils/adt/tsvector_op.c @@ -207,17 +207,10 @@ tsvector_length(PG_FUNCTION_ARGS) PG_RETURN_INT32(ret); } -Datum -tsvector_setweight(PG_FUNCTION_ARGS) +static int +parse_weight(char cw) { - TSVector in = PG_GETARG_TSVECTOR(0); - char cw = PG_GETARG_CHAR(1); - TSVector out; - int i, - j; - WordEntry *entry; - WordEntryPos *p; - int w = 0; + int w; switch (cw) { @@ -238,9 +231,32 @@ tsvector_setweight(PG_FUNCTION_ARGS) w = 0; break; default: - /* internal error */ - elog(ERROR, "unrecognized weight: %d", cw); + /* Avoid printing non-ASCII bytes, else we have encoding issues */ + if (cw >= ' ' && cw < 0x7f) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized weight: \"%c\"", cw))); + else /* use \ooo format, like charout() */ + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized weight: \"\\%03o\"", + (unsigned char) cw))); } + return w; +} + + +Datum +tsvector_setweight(PG_FUNCTION_ARGS) +{ + TSVector in = PG_GETARG_TSVECTOR(0); + char cw = PG_GETARG_CHAR(1); + TSVector out; + int i, + j; + WordEntry *entry; + WordEntryPos *p; + int w = parse_weight(cw); out = (TSVector) palloc(VARSIZE(in)); memcpy(out, in, VARSIZE(in)); @@ -285,28 +301,7 @@ tsvector_setweight_by_filter(PG_FUNCTION_ARGS) Datum *dlexemes; bool *nulls; - switch (char_weight) - { - case 'A': - case 'a': - weight = 3; - break; - case 'B': - case 'b': - weight = 2; - break; - case 'C': - case 'c': - weight = 1; - break; - case 'D': - case 'd': - weight = 0; - break; - default: - /* internal error */ - elog(ERROR, "unrecognized weight: %c", char_weight); - } + weight = parse_weight(char_weight); tsout = (TSVector) palloc(VARSIZE(tsin)); memcpy(tsout, tsin, VARSIZE(tsin)); @@ -845,29 +840,7 @@ tsvector_filter(PG_FUNCTION_ARGS) errmsg("weight array may not contain nulls"))); char_weight = DatumGetChar(dweights[i]); - switch (char_weight) - { - case 'A': - case 'a': - mask = mask | 8; - break; - case 'B': - case 'b': - mask = mask | 4; - break; - case 'C': - case 'c': - mask = mask | 2; - break; - case 'D': - case 'd': - mask = mask | 1; - break; - default: - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized weight: \"%c\"", char_weight))); - } + mask |= 1 << parse_weight(char_weight); } tsout = (TSVector) palloc0(VARSIZE(tsin)); From 273fe94852b3a7e34fd171e8abdf1481beb302fa Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 5 Jun 2026 07:50:12 +0900 Subject: [PATCH 039/250] Fix off-by-one with NFC recomposition for Hangul U+11A7 (TBASE) The NFC recomposition incorrectly included TBASE as a valid T syllable, which is incorrect based on the Unicode specification (TBASE is one below the start of the range, range beginning at U+11A8). This would cause the TBASE to be silently swallowed in the normalization, leading to an incorrect result. A couple of regression tests are added to check more patterns with Hangul recomposition and decomposition, on top of a test to check the problem with TBASE. Diego has submitted the code fix, and I have written the tests. Author: Diego Frias Co-authored-by: Michael Paquier Discussion: https://postgr.es/m/B92ED640-7D4A-4505-B09F-3548F58CBB16@dzfrias.dev Backpatch-through: 14 --- src/common/unicode_norm.c | 2 +- src/test/regress/expected/unicode.out | 78 +++++++++++++++++++++++++++ src/test/regress/sql/unicode.sql | 20 +++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/common/unicode_norm.c b/src/common/unicode_norm.c index 0bb7523f422..2d80ee3ed70 100644 --- a/src/common/unicode_norm.c +++ b/src/common/unicode_norm.c @@ -236,7 +236,7 @@ recompose_code(uint32 start, uint32 code, uint32 *result) /* Check if two current characters are LV and T */ else if (start >= SBASE && start < (SBASE + SCOUNT) && ((start - SBASE) % TCOUNT) == 0 && - code >= TBASE && code < (TBASE + TCOUNT)) + code > TBASE && code < (TBASE + TCOUNT)) { /* make syllable of form LVT */ uint32 tindex = code - TBASE; diff --git a/src/test/regress/expected/unicode.out b/src/test/regress/expected/unicode.out index 1e06de22649..63e48d3a961 100644 --- a/src/test/regress/expected/unicode.out +++ b/src/test/regress/expected/unicode.out @@ -105,3 +105,81 @@ ORDER BY num; SELECT is_normalized('abc', 'def'); -- run-time error ERROR: invalid normalization form: def +-- Hangul NFC recomposition tests +-- L+V -> LV composition (first and last) +SELECT normalize(U&'\1100\1161', NFC) = U&'\AC00' COLLATE "C" AS hangul_lv_first; + hangul_lv_first +----------------- + t +(1 row) + +SELECT normalize(U&'\1112\1175', NFC) = U&'\D788' COLLATE "C" AS hangul_lv_last; + hangul_lv_last +---------------- + t +(1 row) + +-- LV+T -> LVT composition +SELECT normalize(U&'\AC00\11A8', NFC) = U&'\AC01' COLLATE "C" AS hangul_lvt_first_t; + hangul_lvt_first_t +-------------------- + t +(1 row) + +SELECT normalize(U&'\AC00\11C2', NFC) = U&'\AC1B' COLLATE "C" AS hangul_lvt_last_t; + hangul_lvt_last_t +------------------- + t +(1 row) + +SELECT normalize(U&'\D788\11A8', NFC) = U&'\D789' COLLATE "C" AS hangul_lvt_last_lv; + hangul_lvt_last_lv +-------------------- + t +(1 row) + +-- L+V+T -> LVT composition +SELECT normalize(U&'\1100\1161\11A8', NFC) = U&'\AC01' COLLATE "C" AS hangul_full_lvt; + hangul_full_lvt +----------------- + t +(1 row) + +SELECT normalize(U&'\1112\1175\11C2', NFC) = U&'\D7A3' COLLATE "C" AS hangul_full_lvt; + hangul_full_lvt +----------------- + t +(1 row) + +-- TBASE invalid T syllable +SELECT normalize(U&'\AC00\11A7', NFC) = U&'\AC00\11A7' COLLATE "C" AS hangul_tbase_not_combined; + hangul_tbase_not_combined +--------------------------- + t +(1 row) + +SELECT normalize(U&'\1100\1161\11A7', NFC) = U&'\AC00\11A7' COLLATE "C" AS hangul_lv_tbase_separate; + hangul_lv_tbase_separate +-------------------------- + t +(1 row) + +-- Hangul NFD decomposition tests +SELECT normalize(U&'\AC00', NFD) = U&'\1100\1161' COLLATE "C" AS hangul_nfd_lv; + hangul_nfd_lv +--------------- + t +(1 row) + +SELECT normalize(U&'\AC01', NFD) = U&'\1100\1161\11A8' COLLATE "C" AS hangul_nfd_lvt; + hangul_nfd_lvt +---------------- + t +(1 row) + +SELECT normalize(U&'\D7A3', NFD) = U&'\1112\1175\11C2' COLLATE "C" AS hangul_nfd_last; + hangul_nfd_last +----------------- + t +(1 row) + diff --git a/src/test/regress/sql/unicode.sql b/src/test/regress/sql/unicode.sql index e50adb68ed0..951f86a336e 100644 --- a/src/test/regress/sql/unicode.sql +++ b/src/test/regress/sql/unicode.sql @@ -36,3 +36,23 @@ FROM ORDER BY num; SELECT is_normalized('abc', 'def'); -- run-time error + +-- Hangul NFC recomposition tests +-- L+V -> LV composition (first and last) +SELECT normalize(U&'\1100\1161', NFC) = U&'\AC00' COLLATE "C" AS hangul_lv_first; +SELECT normalize(U&'\1112\1175', NFC) = U&'\D788' COLLATE "C" AS hangul_lv_last; +-- LV+T -> LVT composition +SELECT normalize(U&'\AC00\11A8', NFC) = U&'\AC01' COLLATE "C" AS hangul_lvt_first_t; +SELECT normalize(U&'\AC00\11C2', NFC) = U&'\AC1B' COLLATE "C" AS hangul_lvt_last_t; +SELECT normalize(U&'\D788\11A8', NFC) = U&'\D789' COLLATE "C" AS hangul_lvt_last_lv; +-- L+V+T -> LVT composition +SELECT normalize(U&'\1100\1161\11A8', NFC) = U&'\AC01' COLLATE "C" AS hangul_full_lvt; +SELECT normalize(U&'\1112\1175\11C2', NFC) = U&'\D7A3' COLLATE "C" AS hangul_full_lvt; +-- TBASE invalid T syllable +SELECT normalize(U&'\AC00\11A7', NFC) = U&'\AC00\11A7' COLLATE "C" AS hangul_tbase_not_combined; +SELECT normalize(U&'\1100\1161\11A7', NFC) = U&'\AC00\11A7' COLLATE "C" AS hangul_lv_tbase_separate; + +-- Hangul NFD decomposition tests +SELECT normalize(U&'\AC00', NFD) = U&'\1100\1161' COLLATE "C" AS hangul_nfd_lv; +SELECT normalize(U&'\AC01', NFD) = U&'\1100\1161\11A8' COLLATE "C" AS hangul_nfd_lvt; +SELECT normalize(U&'\D7A3', NFD) = U&'\1112\1175\11C2' COLLATE "C" AS hangul_nfd_last; From 79a506228bef41339ba64d27a47e7756dc1fd8db Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Fri, 5 Jun 2026 12:08:05 -0500 Subject: [PATCH 040/250] refint: Remove plan cache. Presently, refint stores plans in a per-backend cache to avoid re-preparing in each call. This has a few problems. For one, check_foreign_key() embeds the new key values in its cascade-UPDATE queries, so a cached plan reuses the values from preparation. Also, the cache is never invalidated, so it can return stale entries that cause other problems. There may very well be more bugs lurking. We could spend a lot of time trying to address all these problems, but this module is primarily intended as sample code, and by all indications, it sees minimal use. Furthermore, there is a growing consensus for removing refint in v20. However, since we'll need to support it on the back-branches for a while longer, it probably still makes sense to fix some of the more egregious bugs. Therefore, let's just remove refint's plan cache entirely. That means we'll re-prepare on every call, but that seems quite unlikely to bother anyone. On v17 and older versions, the regression test for triggers fails after this change, so I've borrowed pieces of commit 8cfbdf8f4d to fix it. Author: Ayush Tiwari Discussion: https://postgr.es/m/CAJTYsWXU%2BfhuzrEd_bnrxyGH3%2Bny8QRQC2QHf3ws6s9iki3c2Q%40mail.gmail.com Backpatch-through: 14 --- contrib/spi/refint.c | 350 ++++++++++++++----------------------------- 1 file changed, 115 insertions(+), 235 deletions(-) diff --git a/contrib/spi/refint.c b/contrib/spi/refint.c index 4877614dfcb..3d8dcc22f50 100644 --- a/contrib/spi/refint.c +++ b/contrib/spi/refint.c @@ -12,7 +12,6 @@ #include "commands/trigger.h" #include "executor/spi.h" #include "utils/builtins.h" -#include "utils/memutils.h" #include "utils/rel.h" PG_MODULE_MAGIC_EXT( @@ -20,20 +19,6 @@ PG_MODULE_MAGIC_EXT( .version = PG_VERSION ); -typedef struct -{ - char *ident; - int nplans; - SPIPlanPtr *splan; -} EPlan; - -static EPlan *FPlans = NULL; -static int nFPlans = 0; -static EPlan *PPlans = NULL; -static int nPPlans = 0; - -static EPlan *find_plan(char *ident, EPlan **eplan, int *nplans); - /* * check_primary_key () -- check that key in tuple being inserted/updated * references existing tuple in "primary" table. @@ -59,12 +44,12 @@ check_primary_key(PG_FUNCTION_ARGS) Relation rel; /* triggered relation */ HeapTuple tuple = NULL; /* tuple to return */ TupleDesc tupdesc; /* tuple description */ - EPlan *plan; /* prepared plan */ + SPIPlanPtr pplan; /* prepared plan */ Oid *argtypes = NULL; /* key types to prepare execution plan */ bool isnull; /* to know is some column NULL or not */ - char ident[2 * NAMEDATALEN]; /* to identify myself */ int ret; int i; + StringInfoData sql; #ifdef DEBUG_QUERY elog(DEBUG4, "check_primary_key: Enter Function"); @@ -123,16 +108,8 @@ check_primary_key(PG_FUNCTION_ARGS) */ kvals = (Datum *) palloc(nkeys * sizeof(Datum)); - /* - * Construct ident string as TriggerName $ TriggeredRelationId and try to - * find prepared execution plan. - */ - snprintf(ident, sizeof(ident), "%s$%u", trigger->tgname, rel->rd_id); - plan = find_plan(ident, &PPlans, &nPPlans); - - /* if there is no plan then allocate argtypes for preparation */ - if (plan->nplans <= 0) - argtypes = (Oid *) palloc(nkeys * sizeof(Oid)); + /* allocate argtypes for preparation */ + argtypes = (Oid *) palloc(nkeys * sizeof(Oid)); /* For each column in key ... */ for (i = 0; i < nkeys; i++) @@ -161,57 +138,36 @@ check_primary_key(PG_FUNCTION_ARGS) return PointerGetDatum(tuple); } - if (plan->nplans <= 0) /* Get typeId of column */ - argtypes[i] = SPI_gettypeid(tupdesc, fnumber); + /* Get typeId of column */ + argtypes[i] = SPI_gettypeid(tupdesc, fnumber); } + initStringInfo(&sql); + /* - * If we have to prepare plan ... + * Construct query: SELECT 1 FROM _referenced_relation_ WHERE Pkey1 = $1 + * [AND Pkey2 = $2 [...]] */ - if (plan->nplans <= 0) + appendStringInfo(&sql, "select 1 from %s where ", relname); + for (i = 1; i <= nkeys; i++) { - SPIPlanPtr pplan; - StringInfoData sql; - - initStringInfo(&sql); - - /* - * Construct query: SELECT 1 FROM _referenced_relation_ WHERE Pkey1 = - * $1 [AND Pkey2 = $2 [...]] - */ - appendStringInfo(&sql, "select 1 from %s where ", relname); - for (i = 1; i <= nkeys; i++) - { - appendStringInfo(&sql, "%s = $%d ", args[i + nkeys], i); - if (i < nkeys) - appendStringInfoString(&sql, "and "); - } - - /* Prepare plan for query */ - pplan = SPI_prepare(sql.data, nkeys, argtypes); - if (pplan == NULL) - /* internal error */ - elog(ERROR, "check_primary_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); + appendStringInfo(&sql, "%s = $%d ", args[i + nkeys], i); + if (i < nkeys) + appendStringInfoString(&sql, "and "); + } - /* - * Remember that SPI_prepare places plan in current memory context - - * so, we have to save plan in TopMemoryContext for later use. - */ - if (SPI_keepplan(pplan)) - /* internal error */ - elog(ERROR, "check_primary_key: SPI_keepplan failed"); - plan->splan = (SPIPlanPtr *) MemoryContextAlloc(TopMemoryContext, - sizeof(SPIPlanPtr)); - *(plan->splan) = pplan; - plan->nplans = 1; + /* Prepare plan for query */ + pplan = SPI_prepare(sql.data, nkeys, argtypes); + if (pplan == NULL) + /* internal error */ + elog(ERROR, "check_primary_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); - pfree(sql.data); - } + pfree(sql.data); /* * Ok, execute prepared plan. */ - ret = SPI_execp(*(plan->splan), kvals, NULL, 1); + ret = SPI_execp(pplan, kvals, NULL, 1); /* we have no NULLs - so we pass ^^^^ here */ if (ret < 0) @@ -263,15 +219,15 @@ check_foreign_key(PG_FUNCTION_ARGS) HeapTuple trigtuple = NULL; /* tuple to being changed */ HeapTuple newtuple = NULL; /* tuple to return */ TupleDesc tupdesc; /* tuple description */ - EPlan *plan; /* prepared plan(s) */ + SPIPlanPtr *splan; /* prepared plan(s) */ Oid *argtypes = NULL; /* key types to prepare execution plan */ bool isnull; /* to know is some column NULL or not */ bool isequal = true; /* are keys in both tuples equal (in UPDATE) */ - char ident[2 * NAMEDATALEN]; /* to identify myself */ int is_update = 0; int ret; int i, r; + char **args2; #ifdef DEBUG_QUERY elog(DEBUG4, "check_foreign_key: Enter Function"); @@ -350,24 +306,8 @@ check_foreign_key(PG_FUNCTION_ARGS) */ kvals = (Datum *) palloc(nkeys * sizeof(Datum)); - /* - * Construct ident string as TriggerName $ TriggeredRelationId $ - * OperationType and try to find prepared execution plan(s). - */ - snprintf(ident, sizeof(ident), "%s$%u$%c", trigger->tgname, rel->rd_id, is_update ? 'U' : 'D'); - plan = find_plan(ident, &FPlans, &nFPlans); - - /* if there is no plan(s) then allocate argtypes for preparation */ - if (plan->nplans <= 0) - argtypes = (Oid *) palloc(nkeys * sizeof(Oid)); - - /* - * else - check that we have exactly nrefs plan(s) ready - */ - else if (plan->nplans != nrefs) - /* internal error */ - elog(ERROR, "%s: check_foreign_key: # of plans changed in meantime", - trigger->tgname); + /* allocate argtypes for preparation */ + argtypes = (Oid *) palloc(nkeys * sizeof(Oid)); /* For each column in key ... */ for (i = 0; i < nkeys; i++) @@ -415,141 +355,124 @@ check_foreign_key(PG_FUNCTION_ARGS) isequal = false; } - if (plan->nplans <= 0) /* Get typeId of column */ - argtypes[i] = SPI_gettypeid(tupdesc, fnumber); + /* Get typeId of column */ + argtypes[i] = SPI_gettypeid(tupdesc, fnumber); } args_temp = args; nargs -= nkeys; args += nkeys; + args2 = args; - /* - * If we have to prepare plans ... - */ - if (plan->nplans <= 0) + splan = (SPIPlanPtr *) palloc(nrefs * sizeof(SPIPlanPtr)); + + for (r = 0; r < nrefs; r++) { + StringInfoData sql; SPIPlanPtr pplan; - char **args2 = args; - plan->splan = (SPIPlanPtr *) MemoryContextAlloc(TopMemoryContext, - nrefs * sizeof(SPIPlanPtr)); + initStringInfo(&sql); - for (r = 0; r < nrefs; r++) + relname = args2[0]; + + /*--------- + * For 'R'estrict action we construct SELECT query: + * + * SELECT 1 + * FROM _referencing_relation_ + * WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]] + * + * to check is tuple referenced or not. + *--------- + */ + if (action == 'r') + appendStringInfo(&sql, "select 1 from %s where ", relname); + + /*--------- + * For 'C'ascade action we construct DELETE query + * + * DELETE + * FROM _referencing_relation_ + * WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]] + * + * to delete all referencing tuples. + *--------- + */ + + /* + * Max : Cascade with UPDATE query i create update query that updates + * new key values in referenced tables + */ + + + else if (action == 'c') { - StringInfoData sql; - - initStringInfo(&sql); - - relname = args2[0]; - - /*--------- - * For 'R'estrict action we construct SELECT query: - * - * SELECT 1 - * FROM _referencing_relation_ - * WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]] - * - * to check is tuple referenced or not. - *--------- - */ - if (action == 'r') - appendStringInfo(&sql, "select 1 from %s where ", relname); - - /*--------- - * For 'C'ascade action we construct DELETE query - * - * DELETE - * FROM _referencing_relation_ - * WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]] - * - * to delete all referencing tuples. - *--------- - */ - - /* - * Max : Cascade with UPDATE query i create update query that - * updates new key values in referenced tables - */ - - - else if (action == 'c') + if (is_update == 1) { - if (is_update == 1) - { - int fn; - char *nv; - int k; - - appendStringInfo(&sql, "update %s set ", relname); - for (k = 1; k <= nkeys; k++) - { - fn = SPI_fnumber(tupdesc, args_temp[k - 1]); - Assert(fn > 0); /* already checked above */ - nv = SPI_getvalue(newtuple, tupdesc, fn); - - appendStringInfo(&sql, " %s = %s ", - args2[k], - nv ? quote_literal_cstr(nv) : "NULL"); - if (k < nkeys) - appendStringInfoString(&sql, ", "); - } - appendStringInfoString(&sql, " where "); - } - else - /* DELETE */ - appendStringInfo(&sql, "delete from %s where ", relname); - } + int fn; + char *nv; + int k; - /* - * For 'S'etnull action we construct UPDATE query - UPDATE - * _referencing_relation_ SET Fkey1 null [, Fkey2 null [...]] - * WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]] - to set key columns in - * all referencing tuples to NULL. - */ - else if (action == 's') - { appendStringInfo(&sql, "update %s set ", relname); - for (i = 1; i <= nkeys; i++) + for (k = 1; k <= nkeys; k++) { - appendStringInfo(&sql, "%s = null", args2[i]); - if (i < nkeys) + fn = SPI_fnumber(tupdesc, args_temp[k - 1]); + Assert(fn > 0); /* already checked above */ + nv = SPI_getvalue(newtuple, tupdesc, fn); + + appendStringInfo(&sql, " %s = %s ", + args2[k], + nv ? quote_literal_cstr(nv) : "NULL"); + if (k < nkeys) appendStringInfoString(&sql, ", "); } appendStringInfoString(&sql, " where "); } + else + /* DELETE */ + appendStringInfo(&sql, "delete from %s where ", relname); + } - /* Construct WHERE qual */ + /* + * For 'S'etnull action we construct UPDATE query - UPDATE + * _referencing_relation_ SET Fkey1 null [, Fkey2 null [...]] WHERE + * Fkey1 = $1 [AND Fkey2 = $2 [...]] - to set key columns in all + * referencing tuples to NULL. + */ + else if (action == 's') + { + appendStringInfo(&sql, "update %s set ", relname); for (i = 1; i <= nkeys; i++) { - appendStringInfo(&sql, "%s = $%d ", args2[i], i); + appendStringInfo(&sql, "%s = null", args2[i]); if (i < nkeys) - appendStringInfoString(&sql, "and "); + appendStringInfoString(&sql, ", "); } + appendStringInfoString(&sql, " where "); + } - /* Prepare plan for query */ - pplan = SPI_prepare(sql.data, nkeys, argtypes); - if (pplan == NULL) - /* internal error */ - elog(ERROR, "check_foreign_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); + /* Construct WHERE qual */ + for (i = 1; i <= nkeys; i++) + { + appendStringInfo(&sql, "%s = $%d ", args2[i], i); + if (i < nkeys) + appendStringInfoString(&sql, "and "); + } - /* - * Remember that SPI_prepare places plan in current memory context - * - so, we have to save plan in Top memory context for later use. - */ - if (SPI_keepplan(pplan)) - /* internal error */ - elog(ERROR, "check_foreign_key: SPI_keepplan failed"); + /* Prepare plan for query */ + pplan = SPI_prepare(sql.data, nkeys, argtypes); + if (pplan == NULL) + /* internal error */ + elog(ERROR, "check_foreign_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result)); - plan->splan[r] = pplan; + splan[r] = pplan; - args2 += nkeys + 1; /* to the next relation */ + args2 += nkeys + 1; /* to the next relation */ #ifdef DEBUG_QUERY - elog(DEBUG4, "check_foreign_key Debug Query is : %s ", sql.data); + elog(DEBUG4, "check_foreign_key Debug Query is : %s ", sql.data); #endif - pfree(sql.data); - } - plan->nplans = nrefs; + pfree(sql.data); } /* @@ -574,7 +497,7 @@ check_foreign_key(PG_FUNCTION_ARGS) relname = args[0]; - ret = SPI_execp(plan->splan[r], kvals, NULL, tcount); + ret = SPI_execp(splan[r], kvals, NULL, tcount); /* we have no NULLs - so we pass ^^^^ here */ if (ret < 0) @@ -613,46 +536,3 @@ check_foreign_key(PG_FUNCTION_ARGS) return PointerGetDatum((newtuple == NULL) ? trigtuple : newtuple); } - -static EPlan * -find_plan(char *ident, EPlan **eplan, int *nplans) -{ - EPlan *newp; - int i; - MemoryContext oldcontext; - - /* - * All allocations done for the plans need to happen in a session-safe - * context. - */ - oldcontext = MemoryContextSwitchTo(TopMemoryContext); - - if (*nplans > 0) - { - for (i = 0; i < *nplans; i++) - { - if (strcmp((*eplan)[i].ident, ident) == 0) - break; - } - if (i != *nplans) - { - MemoryContextSwitchTo(oldcontext); - return (*eplan + i); - } - *eplan = (EPlan *) repalloc(*eplan, (i + 1) * sizeof(EPlan)); - newp = *eplan + i; - } - else - { - newp = *eplan = (EPlan *) palloc(sizeof(EPlan)); - (*nplans) = i = 0; - } - - newp->ident = pstrdup(ident); - newp->nplans = 0; - newp->splan = NULL; - (*nplans)++; - - MemoryContextSwitchTo(oldcontext); - return newp; -} From a5112c9b62da069c352e58428f54f7a75d971779 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Fri, 5 Jun 2026 22:16:42 +0200 Subject: [PATCH 041/250] doc: Use groups instead of curves in TLS documentation With TLS 1.3 the concept of curves was renamed to groups. Update our wording to use groups instead of curves to make it clear what the underlying GUC can support. This was extracted from a slightly larger patch which also renamed variables to match the new terminology. Given that we are in beta this portion was however left as a future excercise. Author: Evan Si Reviewed-by: Ewan Young Discussion: https://postgr.es/m/23C40DD6-1C47-46FC-A746-8A1D8530AD3E@amazon.com Backpatch-through: 18 --- doc/src/sgml/config.sgml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 18ecdc1120f..607dafcb2ed 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1530,17 +1530,17 @@ include_dir 'conf.d' - Specifies the name of the curve to use in ECDH key - exchange. It needs to be supported by all clients that connect. - Multiple curves can be specified by using a colon-separated list. - It does not need to be the same curve used by the server's Elliptic - Curve key. This parameter can only be set in the + Specifies the named group to use for TLS key + exchange. It needs to be supported by all clients that + connect. Multiple groups can be specified by using a colon-separated + list. It does not need to match the key type used by the server + certificate. This parameter can only be set in the postgresql.conf file or on the server command line. The default is X25519:prime256v1. - OpenSSL names for the most common curves + OpenSSL names for the most common groups are: prime256v1 (NIST P-256), secp384r1 (NIST P-384), From 2b09f8a9110a5de217fa59dfb3215686def7dc36 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Sat, 6 Jun 2026 08:16:40 +0900 Subject: [PATCH 042/250] pg_surgery: Fix off-by-one bug with heap offset heap_force_common() declared a boolean array indexed with an OffsetNumber for a size of MaxHeapTuplesPerPage. OffsetNumbers are 1-based, so an input TID whose offset number equals MaxHeapTuplesPerPage wrote one byte past the end of the stack array, crashing the server. Like heapam_handler.c, this commit changes the array so as it uses a 0-based index, substracting one from the OffsetNumbers. Reported-by: Wang Yuelin Reviewed-by: Ashutosh Sharma Discussion: https://postgr.es/m/20260604002256.40f1fd544@smtp.qiye.163.com Backpatch-through: 14 --- contrib/pg_surgery/heap_surgery.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/pg_surgery/heap_surgery.c b/contrib/pg_surgery/heap_surgery.c index 3e86283beb7..602aca66c60 100644 --- a/contrib/pg_surgery/heap_surgery.c +++ b/contrib/pg_surgery/heap_surgery.c @@ -228,8 +228,8 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) } /* Mark it for processing. */ - Assert(offno < MaxHeapTuplesPerPage); - include_this_tid[offno] = true; + Assert(offno <= MaxHeapTuplesPerPage); + include_this_tid[offno - 1] = true; } /* @@ -247,7 +247,7 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) { ItemId itemid; - if (!include_this_tid[curoff]) + if (!include_this_tid[curoff - 1]) continue; itemid = PageGetItemId(page, curoff); From 07a6c262beeec418526672ff62d7da301100c34d Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 8 Jun 2026 14:37:56 +0900 Subject: [PATCH 043/250] psql: Fix expanded aligned output When a table's columns are narrower than the record header line, the expanded aligned format produced misaligned output because the data column width was not adjusted to match the record header width, leading to output like: +-[ RECORD 1 ]-+ | a | 10 | | b | 20 | +---+----+ This commit adjusts the output so as the column width match with the header line, giving: +-[ RECORD 1 ]-+ | a | 10 | | b | 20 | +---+----------+ Author: Pavel Stehule Reviewed-by: Chao Li Discussion: https://postgr.es/m/CAFj8pRCzGpsr9zTHbtTd4mGh2YPJqOEgLgt8JLiopuYA9_1xGw@mail.gmail.com Backpatch-through: 14 --- src/fe_utils/print.c | 7 ++++--- src/test/regress/expected/psql.out | 26 ++++++++++++++++++++++++++ src/test/regress/sql/psql.sql | 11 +++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c index 4af0f32f2fc..4e135aba60e 100644 --- a/src/fe_utils/print.c +++ b/src/fe_utils/print.c @@ -1458,9 +1458,10 @@ print_aligned_vertical(const printTableContent *cont, } /* - * Calculate available width for data in wrapped mode + * Determine data column width: fit output width in wrapped mode, or + * ensure alignment with the record header line in aligned mode. */ - if (cont->opt->format == PRINT_WRAPPED) + if (cont->opt->format == PRINT_WRAPPED || cont->opt->format == PRINT_ALIGNED) { unsigned int swidth, rwidth = 0, @@ -1532,7 +1533,7 @@ print_aligned_vertical(const printTableContent *cont, if (width < rwidth) width = rwidth; - if (output_columns > 0) + if (cont->opt->format == PRINT_WRAPPED && output_columns > 0) { unsigned int min_width; diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index a79325e8a2f..04a16527f69 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -2822,6 +2822,32 @@ execute q; +------------------+-------------------+ deallocate q; +-- expanded output with short-width columns +\pset border 2 +\pset expanded on +create table psql_short_tab(a int, b int); +insert into psql_short_tab values(10,20),(30,40); +\pset format aligned +select * from psql_short_tab; ++-[ RECORD 1 ]-+ +| a | 10 | +| b | 20 | ++-[ RECORD 2 ]-+ +| a | 30 | +| b | 40 | ++---+----------+ + +\pset format wrapped +select * from psql_short_tab; ++-[ RECORD 1 ]-+ +| a | 10 | +| b | 20 | ++-[ RECORD 2 ]-+ +| a | 30 | +| b | 40 | ++---+----------+ + +drop table psql_short_tab; \pset linestyle ascii \pset border 1 -- support table for output-format tests (useful to create a footer) diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index f064e4f5456..5deff417900 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -483,6 +483,17 @@ execute q; deallocate q; +-- expanded output with short-width columns +\pset border 2 +\pset expanded on +create table psql_short_tab(a int, b int); +insert into psql_short_tab values(10,20),(30,40); +\pset format aligned +select * from psql_short_tab; +\pset format wrapped +select * from psql_short_tab; +drop table psql_short_tab; + \pset linestyle ascii \pset border 1 From 4154a148206375e4af136558953e78dd2a5398ef Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 8 Jun 2026 15:29:19 +0900 Subject: [PATCH 044/250] Fix memory leak in pgstat_progress_parallel_incr_param() When called from a parallel worker, this function calls initStringInfo() and pq_beginmessage(), causing a StringInfo allocation to happen twice. pq_endmessage() frees only the second allocation, with each call leaking ~1 kB into the per-worker memory context. This could cause a few hundred megabytes worth of memory to pile up until the worker exits (the message allocations happen in the parallel worker context), with the situation being worse the longer a parallel worker runs. Oversight in f1889729dd3. Author: Baji Shaik Reviewed-by: Sami Imseih Reviewed-by: Tristan Partin Discussion: https://postgr.es/m/CA+fm-RMopta1Dmq8udiU5sp+zwTvhUf4+xfbr3rZDfczH+p-xw@mail.gmail.com Backpatch-through: 17 --- src/backend/utils/activity/backend_progress.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/utils/activity/backend_progress.c b/src/backend/utils/activity/backend_progress.c index 99a8c73bf04..37caeb96fac 100644 --- a/src/backend/utils/activity/backend_progress.c +++ b/src/backend/utils/activity/backend_progress.c @@ -99,8 +99,6 @@ pgstat_progress_parallel_incr_param(int index, int64 incr) { static StringInfoData progress_message; - initStringInfo(&progress_message); - pq_beginmessage(&progress_message, PqMsg_Progress); pq_sendint32(&progress_message, index); pq_sendint64(&progress_message, incr); From 081434b0f582f05ea397dd3490f61b7400851e16 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Mon, 8 Jun 2026 17:07:48 +0900 Subject: [PATCH 045/250] ecpg: Reject multiple header items in GET/SET DESCRIPTOR Previously, ecpg accepted multiple descriptor header items in GET DESCRIPTOR and SET DESCRIPTOR, but generated broken C code when they were used. Although the grammar allowed this syntax, the implementation did not actually support it. This commit tightens the ecpg grammar so the header form of GET/SET DESCRIPTOR accepts only a single header item, matching the implementation and preventing generation of broken C code. Also update the documentation synopsis accordingly. Backpatch to all supported versions. Author: Masashi Kamura Reviewed-by: Hayato Kuroda Reviewed-by: Lakshmi G Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/OS9PR01MB13174AD7D1829D0644B6BB90E9447A@OS9PR01MB13174.jpnprd01.prod.outlook.com Backpatch-through: 14 --- doc/src/sgml/ecpg.sgml | 6 +++--- src/interfaces/ecpg/preproc/ecpg.trailer | 12 ++---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml index d6f1161374c..a7aae6ea4e8 100644 --- a/doc/src/sgml/ecpg.sgml +++ b/doc/src/sgml/ecpg.sgml @@ -7314,7 +7314,7 @@ EXEC SQL EXECUTE IMMEDIATE :command; -GET DESCRIPTOR descriptor_name :cvariable = descriptor_header_item [, ... ] +GET DESCRIPTOR descriptor_name :cvariable = descriptor_header_item GET DESCRIPTOR descriptor_name VALUE column_number :cvariable = descriptor_item [, ... ] @@ -7333,7 +7333,7 @@ GET DESCRIPTOR descriptor_name VALU This command has two forms: The first form retrieves - descriptor header items, which apply to the result + descriptor header item, which applies to the result set in its entirety. One example is the row count. The second form, which requires the column number as additional parameter, retrieves information about a particular column. Examples are @@ -7809,7 +7809,7 @@ EXEC SQL SET CONNECTION = con1; -SET DESCRIPTOR descriptor_name descriptor_header_item = value [, ... ] +SET DESCRIPTOR descriptor_name descriptor_header_item = value SET DESCRIPTOR descriptor_name VALUE number descriptor_item = value [, ...] diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 390e7713bfb..f3f63b8622e 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -1421,32 +1421,24 @@ ECPGDeallocateDescr: DEALLOCATE SQL_DESCRIPTOR quoted_ident_stringvar * manipulate a descriptor header */ -ECPGGetDescriptorHeader: SQL_GET SQL_DESCRIPTOR quoted_ident_stringvar ECPGGetDescHeaderItems +ECPGGetDescriptorHeader: SQL_GET SQL_DESCRIPTOR quoted_ident_stringvar ECPGGetDescHeaderItem { @$ = @3; } ; -ECPGGetDescHeaderItems: ECPGGetDescHeaderItem - | ECPGGetDescHeaderItems ',' ECPGGetDescHeaderItem - ; - ECPGGetDescHeaderItem: cvariable '=' desc_header_item { push_assignment(@1, $3); } ; -ECPGSetDescriptorHeader: SET SQL_DESCRIPTOR quoted_ident_stringvar ECPGSetDescHeaderItems +ECPGSetDescriptorHeader: SET SQL_DESCRIPTOR quoted_ident_stringvar ECPGSetDescHeaderItem { @$ = @3; } ; -ECPGSetDescHeaderItems: ECPGSetDescHeaderItem - | ECPGSetDescHeaderItems ',' ECPGSetDescHeaderItem - ; - ECPGSetDescHeaderItem: desc_header_item '=' IntConstVar { push_assignment(@3, $1); From be176e0a6d38bf9007b2192a404f9661a5b5b10a Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 8 Jun 2026 10:33:52 -0500 Subject: [PATCH 046/250] doc: Expand on proper use of refint. The security team has received a couple of reports about potential SQL injection via refint's trigger arguments. We discussed this while preparing CVE-2026-6637 and concluded that forcibly quoting these arguments is more likely to break working code than to prevent exploits. Unlike data values, the table/column names come from trigger arguments, and there is little reason for a trigger author to put hostile inputs into those arguments. So, let's document it accordingly. Reported-by: Nikolay Samokhvalov Reported-by: Alex Young Reported-by: Satyanarayana Narlapuram Suggested-by: Noah Misch Reviewed-by: Noah Misch Reviewed-by: Fujii Masao Reviewed-by: Christoph Berg Reviewed-by: Satyanarayana Narlapuram Discussion: https://postgr.es/m/ahXP7z7nsfGPOZ3T%40nathan Backpatch-through: 14 --- doc/src/sgml/contrib-spi.sgml | 58 ++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/contrib-spi.sgml b/doc/src/sgml/contrib-spi.sgml index 6fa9479d1b9..7e4e580bc74 100644 --- a/doc/src/sgml/contrib-spi.sgml +++ b/doc/src/sgml/contrib-spi.sgml @@ -34,6 +34,14 @@ key mechanism, of course, but the module is still useful as an example.) + + + refint requires a + secure schema usage pattern and + data types where the equality operator is named =. + + + check_primary_key() checks the referencing table. To use, create an AFTER INSERT OR UPDATE trigger using this @@ -44,6 +52,29 @@ keys, create a trigger for each reference. + + + The referenced table name and column name arguments to + check_primary_key() are copied as-is into internally + generated SQL statements and therefore must be double-quoted by the user as + necessary in the CREATE TRIGGER command. See + for more information about quoting + SQL identifiers. Conversely, the referencing table + column name arguments should not be double quoted. See the following mock + example of proper use of check_primary_key(): + +CREATE TRIGGER mytrigger +AFTER INSERT OR UPDATE ON referencing_table +FOR EACH ROW EXECUTE PROCEDURE +check_primary_key ( + 'column A', 'column B', -- referencing table columns + 'myschema."referenced table"', -- referenced table + '"column A"', '"column B"' -- referenced table columns +); + + + + check_foreign_key() checks the referenced table. To use, create an AFTER DELETE OR UPDATE trigger using this @@ -53,13 +84,38 @@ (cascade — to delete the referencing row, restrict — to abort transaction if referencing keys exist, setnull — to set referencing key fields to null), - the triggered table's column names which form the primary/unique key, then + the referenced table's column names which form the primary/unique key, then the referencing table name and column names (repeated for as many referencing tables as were specified by first argument). Note that the primary/unique key columns should be marked NOT NULL and should have a unique index. + + + The referencing table name and column name arguments + to check_foreign_key() are copied as-is into + internally generated SQL statements and therefore must be double-quoted by + the user as necessary in the CREATE TRIGGER command. + See for more information about + quoting SQL identifiers. Conversely, the referenced + table column name arguments should not be double quoted. See the following + mock example of proper use of check_foreign_key(): + +CREATE TRIGGER mytrigger +AFTER DELETE OR UPDATE ON referenced_table +FOR EACH ROW EXECUTE PROCEDURE +check_foreign_key ( + 1, -- number of referencing tables + 'cascade', -- action + 'column A', 'column B', -- referenced table columns + 'myschema."referencing table"', -- referencing table + '"column A"', '"column B"' -- referencing table columns +); + + + + Note that if these triggers are executed from another BEFORE trigger, they can fail unexpectedly. For From 11aed8d19cd71d2754ac10c71ec668280bd955f5 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 8 Jun 2026 11:48:07 -0400 Subject: [PATCH 047/250] Fix missed checks for hashability of container-type equality. The operators for array_eq, record_eq, range_eq, and multirange_eq are all marked oprcanhash, but there's a pitfall: their hash functions can fail at runtime if the contained type(s) are not hashable. Therefore, the planner has to check hashability of the contained types before deciding it can use hashing in these cases. Not every place had gotten this memo, and noplace at all had considered the issue for ranges or multiranges. In particular we could attempt to use hashing for a ScalarArrayOpExpr on a container type when it won't actually work, leading to "could not identify a hash function ..." runtime failures. For the most part we should fix this in the lookup functions provided by lsyscache.c, to wit get_op_hash_functions and op_hashjoinable. But there's a problem: get_op_hash_functions is not passed the input data type it would need to check. We mustn't change the API of that exported function in a back-patched fix, and even if we wanted to, its call sites in the executor mostly don't have easy access to the required data type OID. Fortunately, the executor call sites don't actually need fixing, because it's expected that the planner verified hashability before building a plan that requires it. Therefore, leave get_op_hash_functions as-is and invent a wrapper function get_op_hash_functions_ext that does the additional checking needed in the planner's uses. We also need to fix hash_ok_operator (extending the fix in 647889667). While at it, neaten up a couple of places in lookup_type_cache where relevant code for multirange cases was written differently from the code for other container types. Note: while this touches pg_operator.dat, it's only to add oid_symbol macros. So there's no on-disk data change and no need for a catversion bump. Reported-by: Andrei Lepikhov Author: Andrei Lepikhov Co-authored-by: Tom Lane Discussion: https://postgr.es/m/ed221f95-f09b-4a9c-b05b-e1fed621ec87@gmail.com Backpatch-through: 14 --- src/backend/optimizer/plan/subselect.c | 4 +- src/backend/optimizer/util/clauses.c | 9 ++- src/backend/utils/cache/lsyscache.c | 85 ++++++++++++++++++++++++-- src/backend/utils/cache/typcache.c | 25 +++----- src/include/catalog/pg_operator.dat | 4 +- src/include/utils/lsyscache.h | 2 + 6 files changed, 102 insertions(+), 27 deletions(-) diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index 0e567890c72..23eace9dbc4 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -837,7 +837,9 @@ hash_ok_operator(OpExpr *expr) if (list_length(expr->args) != 2) return false; if (opid == ARRAY_EQ_OP || - opid == RECORD_EQ_OP) + opid == RECORD_EQ_OP || + opid == RANGE_EQ_OP || + opid == MULTIRANGE_EQ_OP) { /* these are strict, but must check input type to ensure hashable */ Node *leftarg = linitial(expr->args); diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 8e44e92057d..c1dc8644be9 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -2299,7 +2299,8 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) if (IsA(node, ScalarArrayOpExpr)) { ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) node; - Expr *arrayarg = (Expr *) lsecond(saop->args); + Node *leftarg = (Node *) linitial(saop->args); + Node *arrayarg = (Node *) lsecond(saop->args); Oid lefthashfunc; Oid righthashfunc; @@ -2308,7 +2309,8 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) { if (saop->useOr) { - if (get_op_hash_functions(saop->opno, &lefthashfunc, &righthashfunc) && + if (get_op_hash_functions_ext(saop->opno, exprType(leftarg), + &lefthashfunc, &righthashfunc) && lefthashfunc == righthashfunc) { Datum arrdatum = ((Const *) arrayarg)->constvalue; @@ -2340,7 +2342,8 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * just ensure the lookup items are not in the hash table. */ if (OidIsValid(negator) && - get_op_hash_functions(negator, &lefthashfunc, &righthashfunc) && + get_op_hash_functions_ext(negator, exprType(leftarg), + &lefthashfunc, &righthashfunc) && lefthashfunc == righthashfunc) { Datum arrdatum = ((Const *) arrayarg)->constvalue; diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index f88f53f5e58..366031e96c9 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -476,6 +476,12 @@ get_mergejoin_opfamilies(Oid opno) * * Returns true if able to find the requested operator(s), false if not. * (This indicates that the operator should not have been marked oprcanhash.) + * + * Callers must beware that for container types (arrays, records, ranges) + * this function will succeed for array_eq etc, but the hash function could + * fail at runtime if the contained type(s) are not hashable. If it is + * possible that the operator is one of these, precheck with op_hashjoinable + * or get_op_hash_functions_ext. */ bool get_compatible_hash_operators(Oid opno, @@ -576,6 +582,12 @@ get_compatible_hash_operators(Oid opno, * * Returns true if able to find the requested function(s), false if not. * (This indicates that the operator should not have been marked oprcanhash.) + * + * Callers must beware that for container types (arrays, records, ranges) + * this function will succeed for array_eq etc, but the hash function could + * fail at runtime if the contained type(s) are not hashable. If it is + * possible that the operator is one of these, use get_op_hash_functions_ext + * or precheck with op_hashjoinable. */ bool get_op_hash_functions(Oid opno, @@ -658,6 +670,55 @@ get_op_hash_functions(Oid opno, return result; } +/* + * get_op_hash_functions_ext + * As above, but verify hashability in container-type cases. + * + * As with op_hashjoinable, assume the left input type is sufficient + * to disambiguate container-type cases. + */ +bool +get_op_hash_functions_ext(Oid opno, Oid inputtype, + RegProcedure *lhs_procno, RegProcedure *rhs_procno) +{ + TypeCacheEntry *typentry; + + /* Ensure output args are initialized on failure */ + if (lhs_procno) + *lhs_procno = InvalidOid; + if (rhs_procno) + *rhs_procno = InvalidOid; + + /* As in op_hashjoinable, let the typcache handle the hard cases */ + if (opno == ARRAY_EQ_OP) + { + typentry = lookup_type_cache(inputtype, TYPECACHE_HASH_PROC); + if (typentry->hash_proc != F_HASH_ARRAY) + return false; + } + else if (opno == RECORD_EQ_OP) + { + typentry = lookup_type_cache(inputtype, TYPECACHE_HASH_PROC); + if (typentry->hash_proc != F_HASH_RECORD) + return false; + } + else if (opno == RANGE_EQ_OP) + { + typentry = lookup_type_cache(inputtype, TYPECACHE_HASH_PROC); + if (typentry->hash_proc != F_HASH_RANGE) + return false; + } + else if (opno == MULTIRANGE_EQ_OP) + { + typentry = lookup_type_cache(inputtype, TYPECACHE_HASH_PROC); + if (typentry->hash_proc != F_HASH_MULTIRANGE) + return false; + } + + /* OK, do the normal lookup */ + return get_op_hash_functions(opno, lhs_procno, rhs_procno); +} + /* * get_op_index_interpretation * Given an operator's OID, find out which amcanorder opfamilies it belongs to, @@ -1571,7 +1632,8 @@ op_mergejoinable(Oid opno, Oid inputtype) * For array_eq or record_eq, we can sort if the element or field types * are all sortable. We could implement all the checks for that here, but * the typcache already does that and caches the results too, so let's - * rely on the typcache. + * rely on the typcache. We do not need similar special cases for ranges + * or multiranges, because their subtypes are required to be sortable. */ if (opno == ARRAY_EQ_OP) { @@ -1606,10 +1668,11 @@ op_mergejoinable(Oid opno, Oid inputtype) * Returns true if the operator is hashjoinable. (There must be a suitable * hash opfamily entry for this operator if it is so marked.) * - * In some cases (currently only array_eq), hashjoinability depends on the - * specific input data type the operator is invoked for, so that must be - * passed as well. We currently assume that only one input's type is needed - * to check this --- by convention, pass the left input's data type. + * In some cases (currently array_eq, record_eq, range_eq, multirange_eq), + * hashjoinability depends on the specific input data type the operator is + * invoked for, so that must be passed as well. We currently assume that only + * one input's type is needed to check this --- by convention, pass the left + * input's data type. */ bool op_hashjoinable(Oid opno, Oid inputtype) @@ -1631,6 +1694,18 @@ op_hashjoinable(Oid opno, Oid inputtype) if (typentry->hash_proc == F_HASH_RECORD) result = true; } + else if (opno == RANGE_EQ_OP) + { + typentry = lookup_type_cache(inputtype, TYPECACHE_HASH_PROC); + if (typentry->hash_proc == F_HASH_RANGE) + result = true; + } + else if (opno == MULTIRANGE_EQ_OP) + { + typentry = lookup_type_cache(inputtype, TYPECACHE_HASH_PROC); + if (typentry->hash_proc == F_HASH_MULTIRANGE) + result = true; + } else { /* For all other operators, rely on pg_operator.oprcanhash */ diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c index f9aec38a11f..32c912b82f6 100644 --- a/src/backend/utils/cache/typcache.c +++ b/src/backend/utils/cache/typcache.c @@ -776,8 +776,9 @@ lookup_type_cache(Oid type_id, int flags) HASHSTANDARD_PROC); /* - * As above, make sure hash_array, hash_record, or hash_range will - * succeed. + * As above, make sure hash_array, hash_record, hash_range, or + * hash_multirange will succeed. Here we do need to check the range + * cases. */ if (hash_proc == F_HASH_ARRAY && !array_element_has_hashing(typentry)) @@ -788,12 +789,8 @@ lookup_type_cache(Oid type_id, int flags) else if (hash_proc == F_HASH_RANGE && !range_element_has_hashing(typentry)) hash_proc = InvalidOid; - - /* - * Likewise for hash_multirange. - */ - if (hash_proc == F_HASH_MULTIRANGE && - !multirange_element_has_hashing(typentry)) + else if (hash_proc == F_HASH_MULTIRANGE && + !multirange_element_has_hashing(typentry)) hash_proc = InvalidOid; /* Force update of hash_proc_finfo only if we're changing state */ @@ -825,8 +822,8 @@ lookup_type_cache(Oid type_id, int flags) HASHEXTENDED_PROC); /* - * As above, make sure hash_array_extended, hash_record_extended, or - * hash_range_extended will succeed. + * As above, make sure hash_array_extended, hash_record_extended, + * hash_range_extended, or hash_multirange_extended will succeed. */ if (hash_extended_proc == F_HASH_ARRAY_EXTENDED && !array_element_has_extended_hashing(typentry)) @@ -837,12 +834,8 @@ lookup_type_cache(Oid type_id, int flags) else if (hash_extended_proc == F_HASH_RANGE_EXTENDED && !range_element_has_extended_hashing(typentry)) hash_extended_proc = InvalidOid; - - /* - * Likewise for hash_multirange_extended. - */ - if (hash_extended_proc == F_HASH_MULTIRANGE_EXTENDED && - !multirange_element_has_extended_hashing(typentry)) + else if (hash_extended_proc == F_HASH_MULTIRANGE_EXTENDED && + !multirange_element_has_extended_hashing(typentry)) hash_extended_proc = InvalidOid; /* Force update of proc finfo only if we're changing state */ diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat index 6d9dc1528d6..b722a77a480 100644 --- a/src/include/catalog/pg_operator.dat +++ b/src/include/catalog/pg_operator.dat @@ -3057,7 +3057,7 @@ oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' }, # generic range type operators -{ oid => '3882', descr => 'equal', +{ oid => '3882', oid_symbol => 'RANGE_EQ_OP', descr => 'equal', oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'anyrange', oprright => 'anyrange', oprresult => 'bool', oprcom => '=(anyrange,anyrange)', oprnegate => '<>(anyrange,anyrange)', oprcode => 'range_eq', @@ -3263,7 +3263,7 @@ oprname => '@@', oprleft => 'jsonb', oprright => 'jsonpath', oprresult => 'bool', oprcode => 'jsonb_path_match_opr(jsonb,jsonpath)', oprrest => 'matchingsel', oprjoin => 'matchingjoinsel' }, -{ oid => '2860', descr => 'equal', +{ oid => '2860', oid_symbol => 'MULTIRANGE_EQ_OP', descr => 'equal', oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'anymultirange', oprright => 'anymultirange', oprresult => 'bool', oprcom => '=(anymultirange,anymultirange)', diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index ac57bc8a130..81b152da421 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -86,6 +86,8 @@ extern bool get_compatible_hash_operators(Oid opno, Oid *lhs_opno, Oid *rhs_opno); extern bool get_op_hash_functions(Oid opno, RegProcedure *lhs_procno, RegProcedure *rhs_procno); +extern bool get_op_hash_functions_ext(Oid opno, Oid inputtype, + RegProcedure *lhs_procno, RegProcedure *rhs_procno); extern List *get_op_index_interpretation(Oid opno); extern bool equality_ops_are_compatible(Oid opno1, Oid opno2); extern bool comparison_ops_are_compatible(Oid opno1, Oid opno2); From 89e6484985280b259a2346b4b4805750cf6ac0ce Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Mon, 8 Jun 2026 11:47:40 -0700 Subject: [PATCH 048/250] dict_synonym.c: remove incorrect outlen. Previously, outlen was miscalculated if case_sensitive was false and str_tolower() changed the byte length of the string. If outlen was too large, pnstrdup() would stop at the NUL terminator, preventing overrun. But if outlen was too small, it would cause truncation. Fix by just removing outlen. It was only used in a single site, which could just as well use pstrdup(). Discussion: https://postgre.es/m/1101e1a3afbbabb503317069c40374b82e6f4cac.camel@j-davis.com Reviewed-by: Tristan Partin Backpatch-through: 14 --- src/backend/tsearch/dict_synonym.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backend/tsearch/dict_synonym.c b/src/backend/tsearch/dict_synonym.c index e44bf876f8b..fc39caaddca 100644 --- a/src/backend/tsearch/dict_synonym.c +++ b/src/backend/tsearch/dict_synonym.c @@ -24,7 +24,6 @@ typedef struct { char *in; char *out; - int outlen; uint16 flags; } Syn; @@ -189,7 +188,6 @@ dsynonym_init(PG_FUNCTION_ARGS) d->syn[cur].out = str_tolower(starto, strlen(starto), DEFAULT_COLLATION_OID); } - d->syn[cur].outlen = strlen(starto); d->syn[cur].flags = flags; cur++; @@ -236,7 +234,7 @@ dsynonym_lexize(PG_FUNCTION_ARGS) PG_RETURN_POINTER(NULL); res = palloc0(sizeof(TSLexeme) * 2); - res[0].lexeme = pnstrdup(found->out, found->outlen); + res[0].lexeme = pstrdup(found->out); res[0].flags = found->flags; PG_RETURN_POINTER(res); From c090bef07d13c7d424c2d145ca12c1fea9b9face Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 8 Jun 2026 15:23:48 -0400 Subject: [PATCH 049/250] Remove inappropriate translation marker in getObjectIdentityParts(). Strings built by this function are not supposed to be subject to NLS translation, but commit 6566133c5 missed that memo, so that object identities like "membership of role %s in role %s" were translated. --- src/backend/catalog/objectaddress.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..76d5955e5ed 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -5586,7 +5586,7 @@ getObjectIdentityParts(const ObjectAddress *object, amForm = (Form_pg_auth_members) GETSTRUCT(tup); - appendStringInfo(&buffer, _("membership of role %s in role %s"), + appendStringInfo(&buffer, "membership of role %s in role %s", GetUserNameFromId(amForm->member, false), GetUserNameFromId(amForm->roleid, false)); From 26bd362655bc16e2f86eedb87a7d7c37a1d553d3 Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Mon, 8 Jun 2026 13:10:40 -0700 Subject: [PATCH 050/250] Guard against uninitialized default locale. No known problem today, but defend against issues like dbf217c1c7 in the future. Discussion: https://postgr.es/m/d080287d8d2d14c246c86be2e9eb611fb6b27b11.camel@j-davis.com Reviewed-by: Ayush Tiwari Backpatch-through: 17 --- src/backend/utils/adt/pg_locale.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index cb1744f4d69..78243b2c795 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -1199,7 +1199,13 @@ pg_newlocale_from_collation(Oid collid) bool found; if (collid == DEFAULT_COLLATION_OID) + { + /* should not happen: init_database_collation() not yet run */ + if (default_locale == NULL) + elog(ERROR, "default locale not initialized"); + return default_locale; + } /* * Some callers expect C_COLLATION_OID to succeed even without catalog From beb09e9117353f985c9aad6d5c6761193737da74 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 9 Jun 2026 08:18:41 +0900 Subject: [PATCH 051/250] Use correct type for catalog_xmin Commit 85c17f6 mistakenly declared a variable storing catalog_xmin as XLogRecPtr, even though catalog_xmin is a TransactionId. This caused no functional issue, but the type was clearly incorrect. Therefore, this commit fixes it to use the correct type TransactionId instead, and backpatch to v17 where the issue was introduced. Author: Imran Zaheer Reviewed-by: Ashutosh Bapat Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CA+UBfa=mNeLt-4BFjEP4tqdDsnq+oMqqPr7fd9Wji2_9YXmQdA@mail.gmail.com --- src/backend/replication/logical/slotsync.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c index 3cbcd77d12f..bc42d74fec2 100644 --- a/src/backend/replication/logical/slotsync.c +++ b/src/backend/replication/logical/slotsync.c @@ -282,7 +282,7 @@ update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid, { XLogRecPtr old_confirmed_lsn = slot->data.confirmed_flush; XLogRecPtr old_restart_lsn = slot->data.restart_lsn; - XLogRecPtr old_catalog_xmin = slot->data.catalog_xmin; + TransactionId old_catalog_xmin = slot->data.catalog_xmin; LogicalSlotAdvanceAndCheckSnapState(remote_slot->confirmed_lsn, found_consistent_snapshot); From 91b57eadeb0f1f971aebfc3f9c9045477b0e0438 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 11 Jun 2026 14:29:22 +0900 Subject: [PATCH 052/250] xml2: Fix crash with namespace nodes in xpath_nodeset() pgxmlNodeSetToText() passed nodeTab[i]->doc to xmlNodeDump() without checking the node type, which could cause a crash as a XML_NAMESPACE_DECL maps to a xmlNs struct. The passed-in code would then be dereferenced in xmlNodeDump(). This commit switches the code to render XML_NAMESPACE_DECL nodes with xmlXPathCastNodeToString(), like xpath_table(). Some tests are added, written by me. Author: Andrey Chernyy Co-authored-by: Michael Paquier Discussion: https://postgr.es/m/20260611031436.5afde3cb@andrnote Backpatch-through: 14 --- contrib/xml2/expected/xml2.out | 8 ++++++++ contrib/xml2/expected/xml2_1.out | 8 ++++++++ contrib/xml2/sql/xml2.sql | 3 +++ contrib/xml2/xpath.c | 15 +++++++++++---- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/contrib/xml2/expected/xml2.out b/contrib/xml2/expected/xml2.out index 1906fcf33e2..9078f15f6b3 100644 --- a/contrib/xml2/expected/xml2.out +++ b/contrib/xml2/expected/xml2.out @@ -231,6 +231,14 @@ SELECT xpath_nodeset(article_xml::text, '/article/author|/article/pages', test37 (1 row) +-- namespace node +SELECT xpath_nodeset('', + '//namespace::foo'); + xpath_nodeset +---------------------- + http://icl.com/saxon +(1 row) + -- xpath_list() SELECT xpath_list(article_xml::text, '/article/author|/article/pages') FROM articles; diff --git a/contrib/xml2/expected/xml2_1.out b/contrib/xml2/expected/xml2_1.out index 9a2144d58f5..62e8bd6802a 100644 --- a/contrib/xml2/expected/xml2_1.out +++ b/contrib/xml2/expected/xml2_1.out @@ -175,6 +175,14 @@ SELECT xpath_nodeset(article_xml::text, '/article/author|/article/pages', test37 (1 row) +-- namespace node +SELECT xpath_nodeset('', + '//namespace::foo'); + xpath_nodeset +---------------------- + http://icl.com/saxon +(1 row) + -- xpath_list() SELECT xpath_list(article_xml::text, '/article/author|/article/pages') FROM articles; diff --git a/contrib/xml2/sql/xml2.sql b/contrib/xml2/sql/xml2.sql index 510d18a3679..145c487cbde 100644 --- a/contrib/xml2/sql/xml2.sql +++ b/contrib/xml2/sql/xml2.sql @@ -132,6 +132,9 @@ SELECT xpath_nodeset(article_xml::text, '/article/author|/article/pages', SELECT xpath_nodeset(article_xml::text, '/article/author|/article/pages', 'result', 'item') FROM articles; +-- namespace node +SELECT xpath_nodeset('', + '//namespace::foo'); -- xpath_list() SELECT xpath_list(article_xml::text, '/article/author|/article/pages') diff --git a/contrib/xml2/xpath.c b/contrib/xml2/xpath.c index 2820874cb5e..9227db36c41 100644 --- a/contrib/xml2/xpath.c +++ b/contrib/xml2/xpath.c @@ -149,16 +149,23 @@ pgxmlNodeSetToText(xmlNodeSetPtr nodeset, } else { + xmlNodePtr node = nodeset->nodeTab[i]; + if ((septagname != NULL) && (xmlStrlen(septagname) > 0)) { xmlBufferWriteChar(buf, "<"); xmlBufferWriteCHAR(buf, septagname); xmlBufferWriteChar(buf, ">"); } - xmlNodeDump(buf, - nodeset->nodeTab[i]->doc, - nodeset->nodeTab[i], - 1, 0); + + /* + * XML_NAMESPACE_DECL nodes are xmlNs structs, that cannot + * be processed by xmlNodeDump(). + */ + if (node->type == XML_NAMESPACE_DECL) + xmlBufferWriteCHAR(buf, xmlXPathCastNodeToString(node)); + else + xmlNodeDump(buf, node->doc, node, 1, 0); if ((septagname != NULL) && (xmlStrlen(septagname) > 0)) { From b4bd1385043c4664a7b8894a811ba91a61c6e07f Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 11 Jun 2026 17:29:38 +0900 Subject: [PATCH 053/250] Fix race with timeline selection in logical decoding during promotion During promotion, there is a window where RecoveryInProgress() returns true but the WAL segments of the old timeline have already been removed. A logical decoding could pick up the old timeline in this window when reading a page, failing with the following error: ERROR: requested WAL segment ... has already been removed This issue does not lead to any data correctness issue, as retrying to decode the data works in follow-up decoding attempts. It impacts availability, though. Other WAL page read callbacks have a similar issue, this commit takes care of what should be the noisiest code path: logical decoding with START_REPLICATION in a WAL sender. A TAP test, based on an injection point waiting in the startup process after the segments have been removed/recycled, is added. This part is backpatched down to v17. This issue has been causing sporadic failures in the buildfarm, and was reproducible manually. This issue happens since logical decoding on standbys exists, down to v16. Reported-by: Alexander Lakhin Author: Bertrand Drouvot Reviewed-by: Hayato Kuroda Reviewed-by: Xuneng Zhou Discussion: https://postgr.es/m/7daef094-abf3-4672-bc23-3df4763b16a3@gmail.com Backpatch-through: 16 --- src/backend/access/transam/xlog.c | 2 + src/backend/replication/walsender.c | 24 ++++++- .../t/035_standby_logical_decoding.pl | 69 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 47c04f26d40..4f74400f3dd 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -6180,6 +6180,8 @@ StartupXLOG(void) if (ArchiveRecoveryRequested) CleanupAfterArchiveRecovery(EndOfLogTLI, EndOfLog, newTLI); + INJECTION_POINT("promotion-after-wal-segment-cleanup", NULL); + /* * Local WAL inserts enabled, so it's time to finish initialization of * commit timestamp. diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 70d90699cc6..47aec4cd2b2 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -1068,7 +1068,29 @@ logical_read_xlog_page(XLogReaderState *state, XLogRecPtr targetPagePtr, int req am_cascading_walsender = RecoveryInProgress(); if (am_cascading_walsender) - GetXLogReplayRecPtr(&currTLI); + { + TimeLineID insertTLI; + + /* + * If the insertion timeline has already been set, use it. + * InsertTimeLineID is set before the WAL segments of the old timeline + * are removed, before SharedRecoveryState switches to + * RECOVERY_STATE_DONE. + * + * There is a window where RecoveryInProgress() still returns true but + * the old timeline's WAL segments have already been removed or + * recycled. Using the WAL insertion timeline avoids attempting to + * read from those removed segments, improving availability, and is a + * safe thing to do as promotion copies the contents in the last + * segment of the old timeline to the first segment of the new + * timeline, up to the switchpoint. + */ + insertTLI = GetWALInsertionTimeLineIfSet(); + if (insertTLI != 0) + currTLI = insertTLI; + else + GetXLogReplayRecPtr(&currTLI); + } else currTLI = GetWALInsertionTimeLine(); diff --git a/src/test/recovery/t/035_standby_logical_decoding.pl b/src/test/recovery/t/035_standby_logical_decoding.pl index c9c182892cf..d8acc8c173e 100644 --- a/src/test/recovery/t/035_standby_logical_decoding.pl +++ b/src/test/recovery/t/035_standby_logical_decoding.pl @@ -1054,4 +1054,73 @@ BEGIN 'got same expected output from pg_recvlogical decoding session on cascading standby' ); +################################################## +# Test that logical decoding on standby correctly handles a timeline +# change during promotion. This relies on an injection point that +# waits between the moment the segments of the old timeline are removed +# and the moment RecoveryInProgress() would set, catching that a WAL +# sender is still able to decode changes across a promotion. +################################################## + +# Create a logical slot on the cascading standby for this test. +$node_cascading_standby->create_logical_slot_on_standby($node_standby, + 'race_slot', 'testdb'); + +$node_standby->safe_psql('testdb', + qq[INSERT INTO decoding_test(x,y) SELECT s, s::text FROM generate_series(10,13) s;] +); +$node_standby->wait_for_replay_catchup($node_cascading_standby); + +$expected = q{BEGIN +table public.decoding_test: INSERT: x[integer]:10 y[text]:'10' +table public.decoding_test: INSERT: x[integer]:11 y[text]:'11' +table public.decoding_test: INSERT: x[integer]:12 y[text]:'12' +table public.decoding_test: INSERT: x[integer]:13 y[text]:'13' +COMMIT}; + +$node_standby->safe_psql('testdb', 'CREATE EXTENSION injection_points;'); +$node_standby->wait_for_replay_catchup($node_cascading_standby); + +# Attach injection point to pause startup after WAL segment cleanup +# but before RecoveryInProgress() flips to false. +$node_cascading_standby->safe_psql('testdb', + "SELECT injection_points_attach('promotion-after-wal-segment-cleanup', 'wait');" +); + +# Promote, wait for the removal of the segments on the old timeline. +$node_cascading_standby->safe_psql('testdb', "SELECT pg_promote(false)"); +$node_cascading_standby->wait_for_event('startup', + 'promotion-after-wal-segment-cleanup'); + +# Start pg_recvlogical. +my ($stdout2, $stderr2); +my $handle2 = IPC::Run::start( + [ + 'pg_recvlogical', + '--dbname' => $node_cascading_standby->connstr('testdb'), + '--slot' => 'race_slot', + '--option' => 'include-xids=0', + '--option' => 'skip-empty-xacts=1', + '--file' => '-', + '--no-loop', + '--start', + ], + '>' => \$stdout2, + '2>' => \$stderr2, + IPC::Run::timeout($default_timeout)); + +# Verify that pg_recvlogical successfully decodes the data while startup +# is still paused in the injection point. +$pump_timeout = IPC::Run::timer($default_timeout); +ok( pump_until($handle2, $pump_timeout, \$stdout2, qr/COMMIT/s), + 'pg_recvlogical works during promotion timeline switch'); +chomp($stdout2); +is($stdout2, $expected, + 'got expected output from pg_recvlogical during promotion timeline switch' +); + +# Resume promotion. +$node_cascading_standby->safe_psql('testdb', + "SELECT injection_points_wakeup('promotion-after-wal-segment-cleanup');"); + done_testing(); From 0004cab4dc60577779d97a8f1a175b6dd07dc223 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 11 Jun 2026 12:33:48 +0300 Subject: [PATCH 054/250] seg: Fix seg_out() to preserve the upper boundary's certainty indicator When printing the upper boundary of a seg interval, seg_out() decided whether to emit the certainty indicator ('<', '>' or '~') by testing the upper indicator (u_ext) for '<' and '>', but mistakenly tested the lower indicator (l_ext) for '~'. This is a copy-and-paste slip from the symmetric code that prints the lower boundary a few lines above. The consequences for valid input were: * A '~' on the upper boundary was dropped on output, e.g. '1.5 .. ~2.5'::seg printed as '1.5 .. 2.5'. * When the lower boundary carried '~' but the upper boundary had no indicator, the wrong test matched and sprintf(p, "%c", seg->u_ext) wrote a NUL byte (u_ext == '\0'), which truncated the result string and silently lost the entire upper boundary, e.g. '~6.5 .. 8.5'::seg printed as '~6.5 .. '. Certainty indicators are documented to be preserved on output (they are ignored by the operators, but kept as comments), so this broke the input/output round-trip for the affected values. The bug has existed since seg was added. It went unnoticed because the existing regression tests only exercised certainty indicators on single-point segs, which are printed by a different branch of seg_out(). Add tests that place indicators on both boundaries of an interval. Author: Ewan Young Discussion: https://www.postgresql.org/message-id/CAON2xHPYeRRCEVAv8XfE18KsEsEHCiYcJ5fOsoxFuMEfpxF1=g@mail.gmail.com Backpatch-through: 14 --- contrib/seg/expected/seg.out | 45 +++++++++++++++++++++++++++++++++++- contrib/seg/seg.c | 2 +- contrib/seg/sql/seg.sql | 11 ++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/contrib/seg/expected/seg.out b/contrib/seg/expected/seg.out index cd21139b5a7..b7c3fba1597 100644 --- a/contrib/seg/expected/seg.out +++ b/contrib/seg/expected/seg.out @@ -263,7 +263,8 @@ SELECT '12.345678901234560000000000000000000000000000000000000000000000000000000 12.3457 (1 row) --- Numbers with certainty indicators +-- Numbers and ranges with certainty indicators. Certainty indicators +-- are stored and preserved on output, but ignored by operators. SELECT '~6.5'::seg AS seg; seg ------ @@ -300,6 +301,48 @@ SELECT '> 6.5'::seg AS seg; >6.5 (1 row) +SELECT '~1.5 .. 2.5'::seg AS seg; + seg +------------- + ~1.5 .. 2.5 +(1 row) + +SELECT '1.5 .. ~2.5'::seg AS seg; + seg +------------- + 1.5 .. ~2.5 +(1 row) + +SELECT '~1.5 .. ~2.5'::seg AS seg; + seg +-------------- + ~1.5 .. ~2.5 +(1 row) + +SELECT '<1.5 .. 2.5'::seg AS seg; + seg +------------- + <1.5 .. 2.5 +(1 row) + +SELECT '1.5 .. <2.5'::seg AS seg; + seg +------------- + 1.5 .. <2.5 +(1 row) + +SELECT '>1.5 .. 2.5'::seg AS seg; + seg +------------- + >1.5 .. 2.5 +(1 row) + +SELECT '1.5 .. >2.5'::seg AS seg; + seg +------------- + 1.5 .. >2.5 +(1 row) + -- Open intervals SELECT '0..'::seg AS seg; seg diff --git a/contrib/seg/seg.c b/contrib/seg/seg.c index 151cbb954b9..a2cbc82e878 100644 --- a/contrib/seg/seg.c +++ b/contrib/seg/seg.c @@ -152,7 +152,7 @@ seg_out(PG_FUNCTION_ARGS) { /* print the upper boundary if exists */ p += sprintf(p, " "); - if (seg->u_ext == '>' || seg->u_ext == '<' || seg->l_ext == '~') + if (seg->u_ext == '>' || seg->u_ext == '<' || seg->u_ext == '~') p += sprintf(p, "%c", seg->u_ext); p += restore(p, seg->upper, seg->u_sigd); } diff --git a/contrib/seg/sql/seg.sql b/contrib/seg/sql/seg.sql index c30f1f6bef1..a74a42f7e3e 100644 --- a/contrib/seg/sql/seg.sql +++ b/contrib/seg/sql/seg.sql @@ -63,7 +63,8 @@ SELECT '12.34567890123456'::seg AS seg; -- Same, with a very long input SELECT '12.3456789012345600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'::seg AS seg; --- Numbers with certainty indicators +-- Numbers and ranges with certainty indicators. Certainty indicators +-- are stored and preserved on output, but ignored by operators. SELECT '~6.5'::seg AS seg; SELECT '<6.5'::seg AS seg; SELECT '>6.5'::seg AS seg; @@ -71,6 +72,14 @@ SELECT '~ 6.5'::seg AS seg; SELECT '< 6.5'::seg AS seg; SELECT '> 6.5'::seg AS seg; +SELECT '~1.5 .. 2.5'::seg AS seg; +SELECT '1.5 .. ~2.5'::seg AS seg; +SELECT '~1.5 .. ~2.5'::seg AS seg; +SELECT '<1.5 .. 2.5'::seg AS seg; +SELECT '1.5 .. <2.5'::seg AS seg; +SELECT '>1.5 .. 2.5'::seg AS seg; +SELECT '1.5 .. >2.5'::seg AS seg; + -- Open intervals SELECT '0..'::seg AS seg; SELECT '0...'::seg AS seg; From 9108fed3eda97475d5ef2f9060f6bc1f61783346 Mon Sep 17 00:00:00 2001 From: Dean Rasheed Date: Thu, 11 Jun 2026 12:08:48 +0100 Subject: [PATCH 055/250] Fix parsing of parenthesised OLD/NEW in RETURNING list. When parsing expressions like (old).colname and (old).* in a RETURNING list, the parser would lose track of the intended varreturningtype, and therefore return incorrect results. The root cause was code using GetNSItemByRangeTablePosn() to find a namespace item from its rtindex and levelsup, without taking into account returningtype, which would return the wrong namespace item. Fix by adding a new function GetNSItemByVar() that does take returningtype into account. Backpatch to v18, where support for RETURNING OLD/NEW was added. Bug: #19516 Reported-by: Marko Grujic Author: Marko Grujic Suggested-by: Dean Rasheed Reviewed-by: Dean Rasheed Discussion: https://postgr.es/m/CAOvwyF2cO_5mAt=w=y-dFnaG5UkZ+3H8nSDoKF_iuWZHsU2ARg@mail.gmail.com Backpatch-through: 18 --- src/backend/parser/parse_coerce.c | 9 ++++--- src/backend/parser/parse_func.c | 4 +--- src/backend/parser/parse_relation.c | 32 +++++++++++++++++++++++++ src/backend/parser/parse_target.c | 2 +- src/include/parser/parse_relation.h | 1 + src/test/regress/expected/returning.out | 25 +++++++++++++++++++ src/test/regress/sql/returning.sql | 11 +++++++++ 7 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c index 0b5b81c7f27..3900f6edbcf 100644 --- a/src/backend/parser/parse_coerce.c +++ b/src/backend/parser/parse_coerce.c @@ -1034,13 +1034,12 @@ coerce_record_to_complex(ParseState *pstate, Node *node, else if (node && IsA(node, Var) && ((Var *) node)->varattno == InvalidAttrNumber) { - int rtindex = ((Var *) node)->varno; - int sublevels_up = ((Var *) node)->varlevelsup; - int vlocation = ((Var *) node)->location; + Var *var = (Var *) node; ParseNamespaceItem *nsitem; - nsitem = GetNSItemByRangeTablePosn(pstate, rtindex, sublevels_up); - args = expandNSItemVars(pstate, nsitem, sublevels_up, vlocation, NULL); + nsitem = GetNSItemByVar(pstate, var); + args = expandNSItemVars(pstate, nsitem, var->varlevelsup, + var->location, NULL); } else ereport(ERROR, diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 583bbbf232f..cd2bd3784b4 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -1930,9 +1930,7 @@ ParseComplexProjection(ParseState *pstate, const char *funcname, Node *first_arg { ParseNamespaceItem *nsitem; - nsitem = GetNSItemByRangeTablePosn(pstate, - ((Var *) first_arg)->varno, - ((Var *) first_arg)->varlevelsup); + nsitem = GetNSItemByVar(pstate, (Var *) first_arg); /* Return a Var if funcname matches a column, else NULL */ return scanNSItemForColumn(pstate, nsitem, ((Var *) first_arg)->varlevelsup, diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 04ecf64b1fc..b59df7dcdf3 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -513,6 +513,9 @@ check_lateral_ref_ok(ParseState *pstate, ParseNamespaceItem *nsitem, /* * Given an RT index and nesting depth, find the corresponding * ParseNamespaceItem (there must be one). + * + * NB: Callers starting from a Var should consider using GetNSItemByVar() + * instead, to find the namespace item with matching varreturningtype. */ ParseNamespaceItem * GetNSItemByRangeTablePosn(ParseState *pstate, @@ -537,6 +540,35 @@ GetNSItemByRangeTablePosn(ParseState *pstate, return NULL; /* keep compiler quiet */ } +/* + * Given a Var, find the corresponding ParseNamespaceItem (there must be one). + * + * Like GetNSItemByRangeTablePosn(), but uses the Var's varreturningtype in + * addition to its varno and varlevelsup to find the namespace item. + */ +ParseNamespaceItem * +GetNSItemByVar(ParseState *pstate, Var *var) +{ + int sublevels_up = var->varlevelsup; + ListCell *lc; + + while (sublevels_up-- > 0) + { + pstate = pstate->parentParseState; + Assert(pstate != NULL); + } + foreach(lc, pstate->p_namespace) + { + ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(lc); + + if (nsitem->p_rtindex == var->varno && + nsitem->p_returning_type == var->varreturningtype) + return nsitem; + } + elog(ERROR, "nsitem not found (internal error)"); + return NULL; /* keep compiler quiet */ +} + /* * Given an RT index and nesting depth, find the corresponding RTE. * (Note that the RTE need not be in the query's namespace.) diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..336cf66f224 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1446,7 +1446,7 @@ ExpandRowReference(ParseState *pstate, Node *expr, Var *var = (Var *) expr; ParseNamespaceItem *nsitem; - nsitem = GetNSItemByRangeTablePosn(pstate, var->varno, var->varlevelsup); + nsitem = GetNSItemByVar(pstate, var); return ExpandSingleTable(pstate, nsitem, var->varlevelsup, var->location, make_target_entry); } diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index d59599cf242..a05f48317b5 100644 --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -31,6 +31,7 @@ extern void checkNameSpaceConflicts(ParseState *pstate, List *namespace1, extern ParseNamespaceItem *GetNSItemByRangeTablePosn(ParseState *pstate, int varno, int sublevels_up); +extern ParseNamespaceItem *GetNSItemByVar(ParseState *pstate, Var *var); extern RangeTblEntry *GetRTEByRangeTablePosn(ParseState *pstate, int varno, int sublevels_up); diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out index 341b689f766..d5920fcc652 100644 --- a/src/test/regress/expected/returning.out +++ b/src/test/regress/expected/returning.out @@ -540,6 +540,31 @@ DELETE FROM foo WHERE f1 = 5 foo | (0,7) | 5 | ok | 42 | 100 | | | | | | | 5 | ok | 42 | 100 (1 row) +-- Parenthesized OLD and NEW +INSERT INTO foo VALUES (6, 'paren-test', 60, 600) + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; + old | f4 | f1 | f2 | f3 | f4 | new | f4 | f1 | f2 | f3 | f4 +-----+----+----+----+----+----+-----------------------+-----+----+------------+----+----- + | | | | | | (6,paren-test,60,600) | 600 | 6 | paren-test | 60 | 600 +(1 row) + +UPDATE foo SET f4 = 700 WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; + old | f4 | f1 | f2 | f3 | f4 | new | f4 | f1 | f2 | f3 | f4 +-----------------------+-----+----+------------+----+-----+-----------------------+-----+----+------------+----+----- + (6,paren-test,60,600) | 600 | 6 | paren-test | 60 | 600 | (6,paren-test,60,700) | 700 | 6 | paren-test | 60 | 700 +(1 row) + +DELETE FROM foo WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; + old | f4 | f1 | f2 | f3 | f4 | new | f4 | f1 | f2 | f3 | f4 +-----------------------+-----+----+------------+----+-----+-----+----+----+----+----+---- + (6,paren-test,60,700) | 700 | 6 | paren-test | 60 | 700 | | | | | | +(1 row) + -- RETURNING OLD and NEW from subquery EXPLAIN (verbose, costs off) INSERT INTO foo VALUES (5, 'subquery test') diff --git a/src/test/regress/sql/returning.sql b/src/test/regress/sql/returning.sql index cc99cb53f63..133e115851b 100644 --- a/src/test/regress/sql/returning.sql +++ b/src/test/regress/sql/returning.sql @@ -243,6 +243,17 @@ DELETE FROM foo WHERE f1 = 5 RETURNING old.tableoid::regclass, old.ctid, old.*, new.tableoid::regclass, new.ctid, new.*, *; +-- Parenthesized OLD and NEW +INSERT INTO foo VALUES (6, 'paren-test', 60, 600) + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; +UPDATE foo SET f4 = 700 WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; +DELETE FROM foo WHERE f1 = 6 + RETURNING old, (old).f4, (old).*, + new, (new).f4, (new).*; + -- RETURNING OLD and NEW from subquery EXPLAIN (verbose, costs off) INSERT INTO foo VALUES (5, 'subquery test') From 35d9a6263407563f9948a38fced5419deab297a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Thu, 11 Jun 2026 16:17:58 +0200 Subject: [PATCH 056/250] IS JSON/JSON(): Protect against expressions uncoercible to text transformJsonParseArg() was not careful enough on generation of transformed expressions when starting from expressions that are not coercible to text but are in the string type category: it failed to verify that coerce_to_target_type() succeeds, and returned a NULL pointer. This leads to a later NULL dereference and crash at executor time. This escaped noticed because it cannot happen for built-in types, all of which have casts to text. Only user-created types are potentially problematic. Fix by raising an error when a cast to text doesn't exist. This mistake came in with commit 6ee30209a6f1. Author: Ayush Tiwari Reported-by: Chi Zhang <798604270@qq.com> Reviewed-by: Srinath Reddy Sadipiralla Backpatch-through: 16 Discussion: https://postgr.es/m/19491-7aafc221ec63f288@postgresql.org --- src/backend/nodes/makefuncs.c | 2 ++ src/backend/parser/parse_expr.c | 10 +++++++ src/test/regress/expected/sqljson.out | 38 +++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 22 ++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index e2d9e9be41a..bc48966c5d1 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -988,6 +988,8 @@ makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, { JsonIsPredicate *n = makeNode(JsonIsPredicate); + Assert(expr != NULL); + n->expr = expr; n->format = format; n->item_type = item_type; diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index b6d30ddc298..a00b46a25ca 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4088,10 +4088,20 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, if (*exprtype == UNKNOWNOID || typcategory == TYPCATEGORY_STRING) { + int location = exprLocation(expr); + expr = coerce_to_target_type(pstate, (Node *) expr, *exprtype, TEXTOID, -1, COERCION_IMPLICIT, COERCE_IMPLICIT_CAST, -1); + if (expr == NULL) + ereport(ERROR, + errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(*exprtype), + format_type_be(TEXTOID)), + parser_errposition(pstate, location)); + *exprtype = TEXTOID; } diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 625acf3019a..6b0d8815508 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1148,6 +1148,44 @@ SELECT NULL::bytea IS JSON; SELECT NULL::int IS JSON; ERROR: cannot use type integer in IS JSON predicate +-- A user-defined string-category type with no implicit cast to text must +-- produce a clean error rather than crash for IS JSON / JSON() input +-- (per bug #19491). +CREATE FUNCTION sqljson_mystr_in(cstring) RETURNS sqljson_mystr + AS 'textin' LANGUAGE internal IMMUTABLE STRICT; +NOTICE: type "sqljson_mystr" is not yet defined +DETAIL: Creating a shell type definition. +CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cstring + AS 'textout' LANGUAGE internal IMMUTABLE STRICT; +NOTICE: argument type sqljson_mystr is only a shell +LINE 1: CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cst... + ^ +CREATE TYPE sqljson_mystr ( + INPUT = sqljson_mystr_in, + OUTPUT = sqljson_mystr_out, + LIKE = text, + CATEGORY = 'S' +); +SELECT '{"a":1}'::sqljson_mystr IS JSON; -- error +ERROR: cannot cast type sqljson_mystr to text +LINE 1: SELECT '{"a":1}'::sqljson_mystr IS JSON; + ^ +SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS); -- error +ERROR: cannot cast type sqljson_mystr to text +LINE 1: SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS); + ^ +-- An implicit cast to text lets the same query work normally. +CREATE CAST (sqljson_mystr AS text) WITHOUT FUNCTION AS IMPLICIT; +SELECT '{"a":1}'::sqljson_mystr IS JSON; + ?column? +---------- + t +(1 row) + +\set VERBOSITY terse +DROP TYPE sqljson_mystr CASCADE; +NOTICE: drop cascades to 3 other objects +\set VERBOSITY default SELECT '' IS JSON; ?column? ---------- diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 343d344d270..0b77deb3b24 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -395,6 +395,28 @@ SELECT NULL::text IS JSON; SELECT NULL::bytea IS JSON; SELECT NULL::int IS JSON; +-- A user-defined string-category type with no implicit cast to text must +-- produce a clean error rather than crash for IS JSON / JSON() input +-- (per bug #19491). +CREATE FUNCTION sqljson_mystr_in(cstring) RETURNS sqljson_mystr + AS 'textin' LANGUAGE internal IMMUTABLE STRICT; +CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cstring + AS 'textout' LANGUAGE internal IMMUTABLE STRICT; +CREATE TYPE sqljson_mystr ( + INPUT = sqljson_mystr_in, + OUTPUT = sqljson_mystr_out, + LIKE = text, + CATEGORY = 'S' +); +SELECT '{"a":1}'::sqljson_mystr IS JSON; -- error +SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS); -- error +-- An implicit cast to text lets the same query work normally. +CREATE CAST (sqljson_mystr AS text) WITHOUT FUNCTION AS IMPLICIT; +SELECT '{"a":1}'::sqljson_mystr IS JSON; +\set VERBOSITY terse +DROP TYPE sqljson_mystr CASCADE; +\set VERBOSITY default + SELECT '' IS JSON; SELECT bytea '\x00' IS JSON; From 12c32bbc8582727ceb2776c72fa7b5a5f16d8d8c Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 12 Jun 2026 09:35:27 +0900 Subject: [PATCH 057/250] amcheck: Fix missing allequalimage corruption report When amcheck validates that a B-Tree metapage's allequalimage flag matches _bt_allequalimage(), it could fail to report corruption unless one of the index key columns used interval_ops. As a result, pg_amcheck could silently miss this corruption on other opclasses, incorrectly reporting the index as valid. The mistake was that bt_index_check_callback() kept ereport(ERROR) inside the loop that scans key attributes for INTERVAL_BTREE_FAM_OID, even though that loop is only needed to decide whether to add the interval-specific hint. This commit moves ereport() out of the loop so allequalimage mismatches are always reported, while still emitting the hint for affected interval indexes. Back-patch to v18, where d70b17636dd introduced this regression while moving the check into bt_index_check_callback(). Author: Chao Li Reviewed-by: Kirill Reshke Reviewed-by: Xuneng Zhou Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/011ACC9C-CB87-4160-ACE7-4ED57AB86E15@gmail.com Backpatch-through: 18 --- contrib/amcheck/verify_nbtree.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c index 3de1c06c7cf..870954d11b8 100644 --- a/contrib/amcheck/verify_nbtree.c +++ b/contrib/amcheck/verify_nbtree.c @@ -336,14 +336,16 @@ bt_index_check_callback(Relation indrel, Relation heaprel, void *state, bool rea if (indrel->rd_opfamily[i] == INTERVAL_BTREE_FAM_OID) { has_interval_ops = true; - ereport(ERROR, - (errcode(ERRCODE_INDEX_CORRUPTED), - errmsg("index \"%s\" metapage incorrectly indicates that deduplication is safe", - RelationGetRelationName(indrel)), - has_interval_ops - ? errhint("This is known of \"interval\" indexes last built on a version predating 2023-11.") - : 0)); + break; } + + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" metapage incorrectly indicates that deduplication is safe", + RelationGetRelationName(indrel)), + has_interval_ops + ? errhint("This is known of \"interval\" indexes last built on a version predating 2023-11.") + : 0)); } /* Check index, possibly against table it is an index on */ From 4c777d6dd9c9ac8fc8316329745ed1177989379d Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 12 Jun 2026 10:25:49 +0900 Subject: [PATCH 058/250] Fix handling of namespace nodes in xpath() (xml) xpath() attempted to call xmlCopyNode() and xmlNodeDump() on a XML_NAMESPACE_DECL, finishing with a confusing error: =# SELECT xpath('//namespace::foo', ''); ERROR: 53200: could not copy node CONTEXT: SQL function "xpath" statement 1 xpath() is changed so as it goes through xmlXPathCastNodeToString() instead, that is able to handle namespace nodes. xml2 uses the same solution. This issue has been discovered while digging into 9d33a5a804db. Author: Michael Paquier Discussion: https://postgr.es/m/aioT7ui_ZJ9RMlfM@paquier.xyz Backpatch-through: 14 --- src/backend/utils/adt/xml.c | 4 +++- src/test/regress/expected/xml.out | 6 ++++++ src/test/regress/expected/xml_1.out | 5 +++++ src/test/regress/sql/xml.sql | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c index 66803e7058d..02ab4b0da0d 100644 --- a/src/backend/utils/adt/xml.c +++ b/src/backend/utils/adt/xml.c @@ -4156,7 +4156,9 @@ xml_xmlnodetoxmltype(xmlNodePtr cur, PgXmlErrorContext *xmlerrcxt) { xmltype *result = NULL; - if (cur->type != XML_ATTRIBUTE_NODE && cur->type != XML_TEXT_NODE) + if (cur->type != XML_ATTRIBUTE_NODE && + cur->type != XML_TEXT_NODE && + cur->type != XML_NAMESPACE_DECL) { void (*volatile nodefree) (xmlNodePtr) = NULL; volatile xmlBufferPtr buf = NULL; diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out index 103a22a3b1d..e702505b881 100644 --- a/src/test/regress/expected/xml.out +++ b/src/test/regress/expected/xml.out @@ -944,6 +944,12 @@ SELECT xpath('root', ''); {} (1 row) +SELECT xpath('//namespace::foo', ''); + xpath +-------------------- + {http://127.0.0.1} +(1 row) + -- Round-trip non-ASCII data through xpath(). DO $$ DECLARE diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out index 73c411118a3..f39123cc3b0 100644 --- a/src/test/regress/expected/xml_1.out +++ b/src/test/regress/expected/xml_1.out @@ -687,6 +687,11 @@ ERROR: unsupported XML feature LINE 1: SELECT xpath('root', ''); ^ DETAIL: This functionality requires the server to be built with libxml support. +SELECT xpath('//namespace::foo', ''); +ERROR: unsupported XML feature +LINE 1: SELECT xpath('//namespace::foo', ''); -- Round-trip non-ASCII data through xpath(). DO $$ From 556324c386287f5fdd3f6a7848511b46c8fda597 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 12 Jun 2026 11:08:33 +0900 Subject: [PATCH 059/250] doc: fix reference for finding replication slots to drop Commit a70bce43fb added instructions on how to recover if PostgreSQL refuses to issue new transaction IDs because of imminent wraparound, but when describing how to find replication slots that should be dropped, it referred to pg_stat_replication where it should have referenced pg_replication_slots. In passing, decorate references to views with tags. Backpatch to all supported versions. Reported-By: Sanjaya Waruna Author: Laurenz Albe Reviewed-by: Robert Treat Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/176767268098.1084085.10345048667224193115@wrigleys.postgresql.org Backpatch-through: 14 --- doc/src/sgml/maintenance.sgml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 1571a895fc8..ba0d338b48f 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -701,20 +701,20 @@ HINT: Execute a database-wide VACUUM in that database. Resolve old prepared transactions. You can find these by checking - pg_prepared_xacts for rows where + pg_prepared_xacts for rows where age(transactionid) is large. Such transactions should be committed or rolled back. End long-running open transactions. You can find these by checking - pg_stat_activity for rows where + pg_stat_activity for rows where age(backend_xid) or age(backend_xmin) is large. Such transactions should be committed or rolled back, or the session can be terminated using pg_terminate_backend. Drop any old replication slots. Use - pg_stat_replication to + pg_replication_slots to find slots where age(xmin) or age(catalog_xmin) is large. In many cases, such slots were created for replication to servers that no longer exist, or that have been down for a long time. If you drop a slot for a server From 4bff3aa51c194e31044da8177f91eecb0b30205b Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 12 Jun 2026 11:44:14 +0900 Subject: [PATCH 060/250] Fix second race with timeline selection during promotion read_local_xlog_page_guts has the same race as logical_read_xlog_page: RecoveryInProgress() can return true during promotion, impacting the availability of the operations doing WAL page reads with this callback. This problem is similar to eb4e7224a1c6 that has addressed the issue for logical replication, impacting more areas of the code where this WAL page callback can be used (same narrow window during promotion, same availability issue): - pg_walinspect. - Slot advance (SQL function). - Slot creation. Repack workers (v19~) and 2PC files (since forever) can also use this callback, but they are irrelevant as far as I know. A test is added with the SQL lookup functions. This part relies on injection points, and is backpatched down to v18, like the test added for eb4e7224a1c6. This issue could probably be fixed as well in v14 and v15 for pg_walinspect. However, I also feel that there is a conservative argument about consistency here due to the support of logical decoding on standbys, so let's limit ourselves to v16 for now. pg_walinspect is used less in the field compared to the two other operations, making addressing this problem less attractive in these two older branches. Reported-by: Xuneng Zhou Author: Bertrand Drouvot Reviewed-by: Xuneng Zhou Reviewed-by: Hayato Kuroda Discussion: https://postgr.es/m/7daef094-abf3-4672-bc23-3df4763b16a3%40gmail.com Backpatch-through: 16 --- src/backend/access/transam/xlogutils.c | 12 ++++++++++++ src/test/recovery/t/035_standby_logical_decoding.pl | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c index c389b27f77d..db5a314edf8 100644 --- a/src/backend/access/transam/xlogutils.c +++ b/src/backend/access/transam/xlogutils.c @@ -896,7 +896,19 @@ read_local_xlog_page_guts(XLogReaderState *state, XLogRecPtr targetPagePtr, if (!RecoveryInProgress()) read_upto = GetFlushRecPtr(&currTLI); else + { + TimeLineID insertTLI; + read_upto = GetXLogReplayRecPtr(&currTLI); + + /* + * If the insertion timeline has already been set, use it. See + * logical_read_xlog_page() for details. + */ + insertTLI = GetWALInsertionTimeLineIfSet(); + if (insertTLI != 0) + currTLI = insertTLI; + } tli = currTLI; /* diff --git a/src/test/recovery/t/035_standby_logical_decoding.pl b/src/test/recovery/t/035_standby_logical_decoding.pl index d8acc8c173e..bbc33afde3d 100644 --- a/src/test/recovery/t/035_standby_logical_decoding.pl +++ b/src/test/recovery/t/035_standby_logical_decoding.pl @@ -1065,6 +1065,8 @@ BEGIN # Create a logical slot on the cascading standby for this test. $node_cascading_standby->create_logical_slot_on_standby($node_standby, 'race_slot', 'testdb'); +$node_cascading_standby->create_logical_slot_on_standby($node_standby, + 'race_slot_sql', 'testdb'); $node_standby->safe_psql('testdb', qq[INSERT INTO decoding_test(x,y) SELECT s, s::text FROM generate_series(10,13) s;] @@ -1081,6 +1083,10 @@ BEGIN $node_standby->safe_psql('testdb', 'CREATE EXTENSION injection_points;'); $node_standby->wait_for_replay_catchup($node_cascading_standby); +# Open a background psql session BEFORE promotion for the SQL decoding +# test. +my $decode_session = $node_cascading_standby->background_psql('testdb'); + # Attach injection point to pause startup after WAL segment cleanup # but before RecoveryInProgress() flips to false. $node_cascading_standby->safe_psql('testdb', @@ -1119,6 +1125,13 @@ BEGIN 'got expected output from pg_recvlogical during promotion timeline switch' ); +# Verify SQL decoding. +my $sql_out = $decode_session->query_safe( + "SELECT data FROM pg_logical_slot_peek_changes('race_slot_sql', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1')" +); +is($sql_out, $expected, + 'pg_logical_slot_peek_changes works during promotion timeline switch'); + # Resume promotion. $node_cascading_standby->safe_psql('testdb', "SELECT injection_points_wakeup('promotion-after-wal-segment-cleanup');"); From 0d145be2c371a710cd85a5255ae843ee89393076 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 12 Jun 2026 12:37:21 +0900 Subject: [PATCH 061/250] Update expected regression test output for xml_2.out This one has been forgotten in 8bf257aebac1. Per report from buildfarm member massasauga. Backpatch-through: 14 --- src/test/regress/expected/xml_2.out | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out index a85d95358d9..e0ae6395122 100644 --- a/src/test/regress/expected/xml_2.out +++ b/src/test/regress/expected/xml_2.out @@ -930,6 +930,12 @@ SELECT xpath('root', ''); {} (1 row) +SELECT xpath('//namespace::foo', ''); + xpath +-------------------- + {http://127.0.0.1} +(1 row) + -- Round-trip non-ASCII data through xpath(). DO $$ DECLARE From 27cf3b5aff4fdb53d00f44760ecdce06ddb02925 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Fri, 12 Jun 2026 13:57:22 +0200 Subject: [PATCH 062/250] Fix compilation with OpenSSL 4 OpenSSL 4.0.0 changed some parameters and returnvalues to const, so we need to update our declarations and subsequently cast away const- ness from a few callsites to make libpq build without warnings. This is tested with OpenSSL 1.1.1 through 4.0.0 as well as with LibreSSL. No functional change is introduced, this commit only allows postgres to be compiled against OpenSSL 4.0.0 without warnings. There is also an errormessage change in OpenSSL 4.0.0 which needed to be covered by our testharness. This will be backpatched to all supported branches since they are all equally likely to be built against OpenSSL 4.0.0 as it becomes available in distributions. Backpatching will be done once it has been in master for a few days without issues. Author: Daniel Gustafsson Reviewed-by: Michael Paquier Discussion: https://postgr.es/m/066B07BB-85FA-487C-BE8C-40F791CFC3C4@yesql.se Backpatch-through: 14 --- contrib/sslinfo/sslinfo.c | 20 ++++++++++---------- src/backend/libpq/be-secure-openssl.c | 14 +++++++------- src/interfaces/libpq/fe-secure-openssl.c | 9 +++++---- src/test/ssl/t/001_ssltests.pl | 6 +++--- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/contrib/sslinfo/sslinfo.c b/contrib/sslinfo/sslinfo.c index da702011193..9191bbce5dc 100644 --- a/contrib/sslinfo/sslinfo.c +++ b/contrib/sslinfo/sslinfo.c @@ -24,8 +24,8 @@ PG_MODULE_MAGIC_EXT( .version = PG_VERSION ); -static Datum X509_NAME_field_to_text(X509_NAME *name, text *fieldName); -static Datum ASN1_STRING_to_text(ASN1_STRING *str); +static Datum X509_NAME_field_to_text(const X509_NAME *name, text *fieldName); +static Datum ASN1_STRING_to_text(const ASN1_STRING *str); /* * Function context for data persisting over repeated calls. @@ -148,7 +148,7 @@ ssl_client_serial(PG_FUNCTION_ARGS) * function. */ static Datum -ASN1_STRING_to_text(ASN1_STRING *str) +ASN1_STRING_to_text(const ASN1_STRING *str) { BIO *membuf; size_t size; @@ -194,12 +194,12 @@ ASN1_STRING_to_text(ASN1_STRING *str) * part of name */ static Datum -X509_NAME_field_to_text(X509_NAME *name, text *fieldName) +X509_NAME_field_to_text(const X509_NAME *name, text *fieldName) { char *string_fieldname; int nid, index; - ASN1_STRING *data; + const ASN1_STRING *data; string_fieldname = text_to_cstring(fieldName); nid = OBJ_txt2nid(string_fieldname); @@ -209,7 +209,7 @@ X509_NAME_field_to_text(X509_NAME *name, text *fieldName) errmsg("invalid X.509 field name: \"%s\"", string_fieldname))); pfree(string_fieldname); - index = X509_NAME_get_index_by_NID(name, nid, -1); + index = X509_NAME_get_index_by_NID(unconstify(X509_NAME *, name), nid, -1); if (index < 0) return (Datum) 0; data = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(name, index)); @@ -421,8 +421,8 @@ ssl_extension_info(PG_FUNCTION_ARGS) HeapTuple tuple; Datum result; BIO *membuf; - X509_EXTENSION *ext; - ASN1_OBJECT *obj; + const X509_EXTENSION *ext; + const ASN1_OBJECT *obj; int nid; int len; @@ -435,7 +435,7 @@ ssl_extension_info(PG_FUNCTION_ARGS) /* Get the extension from the certificate */ ext = X509_get_ext(cert, call_cntr); - obj = X509_EXTENSION_get_object(ext); + obj = X509_EXTENSION_get_object(unconstify(X509_EXTENSION *, ext)); /* Get the extension name */ nid = OBJ_obj2nid(obj); @@ -448,7 +448,7 @@ ssl_extension_info(PG_FUNCTION_ARGS) nulls[0] = false; /* Get the extension value */ - if (X509V3_EXT_print(membuf, ext, 0, 0) <= 0) + if (X509V3_EXT_print(membuf, unconstify(X509_EXTENSION *, ext), 0, 0) <= 0) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("could not print extension value in certificate at position %d", diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index c8b63ef8249..b67b91a54b2 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -78,7 +78,7 @@ static bool initialize_ecdh(SSL_CTX *context, bool isServerStart); static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement); static const char *SSLerrmessage(unsigned long ecode); -static char *X509_NAME_to_cstring(X509_NAME *name); +static char *X509_NAME_to_cstring(const X509_NAME *name); static SSL_CTX *SSL_context = NULL; static bool dummy_ssl_passwd_cb_called = false; @@ -638,18 +638,18 @@ be_tls_open_server(Port *port) if (port->peer != NULL) { int len; - X509_NAME *x509name = X509_get_subject_name(port->peer); + const X509_NAME *x509name = X509_get_subject_name(port->peer); char *peer_dn; BIO *bio = NULL; BUF_MEM *bio_buf = NULL; - len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0); + len = X509_NAME_get_text_by_NID(unconstify(X509_NAME *, x509name), NID_commonName, NULL, 0); if (len != -1) { char *peer_cn; peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1); - r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn, + r = X509_NAME_get_text_by_NID(unconstify(X509_NAME *, x509name), NID_commonName, peer_cn, len + 1); peer_cn[len] = '\0'; if (r != len) @@ -1642,14 +1642,14 @@ be_tls_get_certificate_hash(Port *port, size_t *len) * */ static char * -X509_NAME_to_cstring(X509_NAME *name) +X509_NAME_to_cstring(const X509_NAME *name) { BIO *membuf = BIO_new(BIO_s_mem()); int i, nid, count = X509_NAME_entry_count(name); - X509_NAME_ENTRY *e; - ASN1_STRING *v; + const X509_NAME_ENTRY *e; + const ASN1_STRING *v; const char *field_name; size_t size; char nullterm; diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index d2045c73ae6..1dd9ba2f506 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -67,7 +67,7 @@ static int verify_cb(int ok, X509_STORE_CTX *ctx); static int openssl_verify_peer_name_matches_certificate_name(PGconn *conn, - ASN1_STRING *name_entry, + const ASN1_STRING *name_entry, char **store_name); static int openssl_verify_peer_name_matches_certificate_ip(PGconn *conn, ASN1_OCTET_STRING *addr_entry, @@ -467,7 +467,8 @@ cert_cb(SSL *ssl, void *arg) * into a plain C string. */ static int -openssl_verify_peer_name_matches_certificate_name(PGconn *conn, ASN1_STRING *name_entry, +openssl_verify_peer_name_matches_certificate_name(PGconn *conn, + const ASN1_STRING *name_entry, char **store_name) { int len; @@ -650,14 +651,14 @@ pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn, */ if (check_cn) { - X509_NAME *subject_name; + const X509_NAME *subject_name; subject_name = X509_get_subject_name(conn->peer); if (subject_name != NULL) { int cn_index; - cn_index = X509_NAME_get_index_by_NID(subject_name, + cn_index = X509_NAME_get_index_by_NID(unconstify(X509_NAME *, subject_name), NID_commonName, -1); if (cn_index >= 0) { diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 310d70a4c08..973399b63d0 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -819,7 +819,7 @@ 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/]* 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"}, @@ -921,7 +921,7 @@ 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/]* 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"}, @@ -932,7 +932,7 @@ 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/]* 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"}, From 10e510423dc3a0e3c696b981b4d12096c5ab6428 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Fri, 12 Jun 2026 18:05:25 -0400 Subject: [PATCH 063/250] Adjust cross-version upgrade tests for seg_out() fix Commit 0e1f1ed157e taught seg_out() to print the certainty indicator on an interval's upper boundary, but it was back-patched only as far as v14. When upgrading from an older release, the old server prints the one test_seg row exercising that case ('4.6 .. ~7.0') without the indicator, so the pre- and post-upgrade dumps do not match. Make AdjustUpgrade.pm delete just that row; seg's comparison function does distinguish the certainty indicators, so the otherwise identical row '4.6 .. 7.0' is unaffected. Back-patch to all supported branches. Per buildfarm members crake and fairywren. Discussion: https://postgr.es/m/5ccbdbde-6467-4a10-bf4d-0be73a05ce8d@dunslane.net --- src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm index 1725fe2f948..ffba1558ff2 100644 --- a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm +++ b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm @@ -167,6 +167,14 @@ sub adjust_database_contents 'drop function if exists public.putenv(text)', 'drop function if exists public.wait_pid(integer)'); } + + # delete seg row that pre-14 was printed incorrectly but would now + # be printed correctly + if ($dbnames{contrib_regression_seg}) + { + _add_st($result, 'contrib_regression_seg', + "delete from test_seg where s = '4.6 .. ~7.0'"); + } } # user table OIDs are gone from release 12 on From 897e79486296d136855734a34de062eadfe3199b Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Sun, 14 Jun 2026 02:49:05 +0300 Subject: [PATCH 064/250] amcheck: Use correct varlena size accessor in bt_normalize_tuple() bt_normalize_tuple() uses VARSIZE() to get the size of varlena, even though it's not yet known, that it has a 4-byte header. Fix this by replacing a accessor with a universal VARSIZE_ANY(). Backpatch to all supported versions. Reported-by: Andres Freund Discussion: https://postgr.es/m/7ckc7oka4bvafkf5bwlqs6ygrhlsbhz25ppozfch7zbuxcx3rf%40e4pr4oqenalc Author: Andrey Borodin Reviewed-by: Alexander Korotkov Backpatch-through: 14 --- contrib/amcheck/verify_nbtree.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c index 870954d11b8..c7cd8d3f043 100644 --- a/contrib/amcheck/verify_nbtree.c +++ b/contrib/amcheck/verify_nbtree.c @@ -2893,7 +2893,7 @@ bt_normalize_tuple(BtreeCheckState *state, IndexTuple itup) ItemPointerGetOffsetNumber(&(itup->t_tid)), RelationGetRelationName(state->rel)))); else if (!VARATT_IS_COMPRESSED(DatumGetPointer(normalized[i])) && - VARSIZE(DatumGetPointer(normalized[i])) > TOAST_INDEX_TARGET && + VARSIZE_ANY(DatumGetPointer(normalized[i])) > TOAST_INDEX_TARGET && (att->attstorage == TYPSTORAGE_EXTENDED || att->attstorage == TYPSTORAGE_MAIN)) { From b4db796b192cdbe6a78c8e3ef3d235c236e09528 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 14 Jun 2026 11:01:48 -0400 Subject: [PATCH 065/250] Doc: remove stale entry for removed aclitem[] ~ aclitem operator. Commit 2f70fdb06 removed the deprecated containment operator ~(aclitem[],aclitem) from the catalogs, but missed removing its entry from the documentation. (Arguably the blame should fall on c62dd80cd, which added this entry in contravention of the longstanding policy that we don't document deprecated aliases in the first place.) Author: Shinya Kato Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAOzEurQSyR5psWukyhUz1LtxyO55C2Vfp0Fmt8w2jGKxhszQmQ@mail.gmail.com Backpatch-through: 14 --- doc/src/sgml/func.sgml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 1128763d04d..3af0a615c04 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25774,20 +25774,6 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); t - - - - aclitem[] ~ aclitem - boolean - - - This is a deprecated alias for @>. - - - '{calvin=r*w/hobbes,hobbes=r*w*/postgres}'::aclitem[] ~ 'calvin=r*/hobbes'::aclitem - t - - From e592535d224f7dd411018e373104ffeb85fcddca Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 15 Jun 2026 11:37:55 +0900 Subject: [PATCH 066/250] Trim regression test expected output for xml This commit reduces the number of expected output files for the "xml" test from three to two (well, mostly one, see below for details). xml_2.out existed to handle some differences in output due to libxml2 2.9.3, due to some error context missing (085423e3e326). This file is removed, by tweaking the XML inputs to trigger the same error patterns for the problematic 2.9.3 and other libxml2 versions. This part is authored by Tom Lane. xml_1.out (no libxml2 support) is reduced in size by adding an \if query that exits the test early. This still checks NO_XML_SUPPORT() through xmlin(). The rest of the test is skipped if XML input cannot be handled by the backend. This part has been written by me. Author: Tom Lane Author: Michael Paquier Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/aiu6CXO67q-s70n5@paquier.xyz Backpatch-through: 14 --- src/test/regress/expected/xml.out | 56 +- src/test/regress/expected/xml_1.out | 1493 +-------------------- src/test/regress/expected/xml_2.out | 1875 --------------------------- src/test/regress/sql/xml.sql | 21 +- 4 files changed, 52 insertions(+), 3393 deletions(-) delete mode 100644 src/test/regress/expected/xml_2.out diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out index e702505b881..7068d0eadcf 100644 --- a/src/test/regress/expected/xml.out +++ b/src/test/regress/expected/xml.out @@ -4,13 +4,19 @@ CREATE TABLE xmltest ( ); INSERT INTO xmltest VALUES (1, 'one'); INSERT INTO xmltest VALUES (2, 'two'); -INSERT INTO xmltest VALUES (3, 'three '); ERROR: invalid XML content -LINE 1: INSERT INTO xmltest VALUES (3, 'three '); ^ -DETAIL: line 1: Couldn't find end of Start Tag wrong line 1 -three + ^ +-- If no XML data could be inserted, skip the tests as the server has been +-- compiled without libxml support. +SELECT count(*) = 0 AS skip_test FROM xmltest \gset +\if :skip_test +\quit +\endif SELECT * FROM xmltest; id | data ----+-------------------- @@ -89,13 +95,13 @@ SELECT xmlconcat(1, 2); ERROR: argument of XMLCONCAT must be type xml, not type integer LINE 1: SELECT xmlconcat(1, 2); ^ -SELECT xmlconcat('bad', ' '); ERROR: invalid XML content -LINE 1: SELECT xmlconcat('bad', ' '); ^ -DETAIL: line 1: Couldn't find end of Start Tag syntax line 1 - + ^ SELECT xmlconcat('', NULL, ''); xmlconcat -------------- @@ -271,13 +277,13 @@ SELECT xmlparse(content ''); (1 row) -SELECT xmlparse(content '&idontexist;'); +SELECT xmlparse(content '&idontexist; '); ERROR: invalid XML content DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; +&idontexist; ^ line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced -&idontexist; +&idontexist; ^ SELECT xmlparse(content ''); xmlparse @@ -285,11 +291,11 @@ SELECT xmlparse(content ''); (1 row) -SELECT xmlparse(document ' '); +SELECT xmlparse(document '!'); ERROR: invalid XML document DETAIL: line 1: Start tag expected, '<' not found - - ^ +! +^ SELECT xmlparse(document 'abc'); ERROR: invalid XML document DETAIL: line 1: Start tag expected, '<' not found @@ -301,21 +307,21 @@ SELECT xmlparse(document 'x'); x (1 row) -SELECT xmlparse(document '&'); +SELECT xmlparse(document '& '); ERROR: invalid XML document DETAIL: line 1: xmlParseEntityRef: no name -& +& ^ line 1: Opening and ending tag mismatch: invalidentity line 1 and abc -& +& ^ -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '&idontexist; '); ERROR: invalid XML document DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; +&idontexist; ^ line 1: Opening and ending tag mismatch: undefinedentity line 1 and abc -&idontexist; +&idontexist; ^ SELECT xmlparse(document ''); xmlparse @@ -329,13 +335,13 @@ SELECT xmlparse(document ''); (1 row) -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '&idontexist; '); ERROR: invalid XML document DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; +&idontexist; ^ line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced -&idontexist; +&idontexist; ^ SELECT xmlparse(document ''); xmlparse diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out index f39123cc3b0..af7f06476f1 100644 --- a/src/test/regress/expected/xml_1.out +++ b/src/test/regress/expected/xml_1.out @@ -12,1492 +12,13 @@ ERROR: unsupported XML feature LINE 1: INSERT INTO xmltest VALUES (2, 'two'); ^ DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest VALUES (3, 'three '); ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (3, 'three '); ^ DETAIL: This functionality requires the server to be built with libxml support. -SELECT * FROM xmltest; - id | data -----+------ -(0 rows) - --- test non-throwing API, too -SELECT pg_input_is_valid('one', 'xml'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT pg_input_is_valid('oneone', 'xml'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT message FROM pg_input_error_info('', 'xml'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('test'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('-test'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('test-'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('--test'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlcomment('te st'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlconcat(xmlcomment('hello'), - xmlelement(NAME qux, 'foo'), - xmlcomment('world')); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlconcat('hello', 'you'); -ERROR: unsupported XML feature -LINE 1: SELECT xmlconcat('hello', 'you'); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlconcat(1, 2); -ERROR: argument of XMLCONCAT must be type xml, not type integer -LINE 1: SELECT xmlconcat(1, 2); - ^ -SELECT xmlconcat('bad', '', NULL, ''); -ERROR: unsupported XML feature -LINE 1: SELECT xmlconcat('', NULL, '', NULL, ''); -ERROR: unsupported XML feature -LINE 1: SELECT xmlconcat('', NULL, 'r'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xml 'br'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, array[1, 2, 3]); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SET xmlbinary TO base64; -SELECT xmlelement(name foo, bytea 'bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SET xmlbinary TO hex; -SELECT xmlelement(name foo, bytea 'bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xmlattributes(true as bar)); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xmlattributes('2009-04-09 00:24:37'::timestamp as bar)); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xmlattributes('infinity'::timestamp as bar)); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlelement(name foo, xmlattributes('<>&"''' as funny, xml 'br' as funnier)); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ' '); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content 'abc'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content 'x'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content '&'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content '&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content '&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(content ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document ' '); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document 'abc'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document 'x'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document '&'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document '&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document '&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlparse(document ''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name xml); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name xmlstuff); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo, 'bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo, 'in?>valid'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo, null); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name xml, null); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name xmlstuff, null); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name "xml-stylesheet", 'href="mystyle.css" type="text/css"'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name foo, ' bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xml '', version no value, standalone no value); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xml '', version no value, standalone no... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xml '', version '2.0'); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xml '', version '2.0'); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xml '', version no value, standalone yes); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xml '', version no value, standalone ye... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xml '', version no value, standalone yes); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xml '', version no... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot(xmlroot(xml '', version '1.0'), version '1.1', standalone no); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot(xmlroot(xml '', version '1.0'), version... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot('', version no value, standalone no); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot('... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot('', version no value, standalone no value); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot('... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot('', version no value); -ERROR: unsupported XML feature -LINE 1: SELECT xmlroot('... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlroot ( - xmlelement ( - name gazonk, - xmlattributes ( - 'val' AS name, - 1 + 1 AS num - ), - xmlelement ( - NAME qux, - 'foo' - ) - ), - version '1.0', - standalone yes -); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(content data as character varying(20)) FROM xmltest; - xmlserialize --------------- -(0 rows) - -SELECT xmlserialize(content 'good' as char(10)); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(content 'good' as char(10)); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(document 'bad' as text); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(document 'bad' as text); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent -SELECT xmlserialize(DOCUMENT '42' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '42' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- no indent -SELECT xmlserialize(DOCUMENT '42' AS text NO INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '42' AS text NO INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent non singly-rooted xml -SELECT xmlserialize(DOCUMENT '7342' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '734... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '7342' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '734... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent non singly-rooted xml with mixed contents -SELECT xmlserialize(DOCUMENT 'text node73text node42' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT 'text node73text nod... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT 'text node73text node42' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT 'text node73text nod... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent singly-rooted xml with mixed contents -SELECT xmlserialize(DOCUMENT '42text node73' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '42text node73' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent empty string -SELECT xmlserialize(DOCUMENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '' AS text INDENT); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '' AS text INDENT); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- whitespaces -SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT ' ' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT ' ' AS text INDENT); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent null -SELECT xmlserialize(DOCUMENT NULL AS text INDENT); - xmlserialize --------------- - -(1 row) - -SELECT xmlserialize(CONTENT NULL AS text INDENT); - xmlserialize --------------- - -(1 row) - --- indent with XML declaration -SELECT xmlserialize(DOCUMENT '73' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '73' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '' AS text INDE... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '' AS text INDE... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent xml with empty element -SELECT xmlserialize(DOCUMENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '' AS tex... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '' AS tex... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- 'no indent' = not using 'no indent' -SELECT xmlserialize(DOCUMENT '42' AS text) = xmlserialize(DOCUMENT '42' AS text NO INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT '42' AS text) = xmlserialize(CONTENT '42' AS text NO INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT '42<... - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- indent xml strings containing blank nodes -SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(DOCUMENT ' '... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlserialize(CONTENT 'text node ' AS text INDENT); -ERROR: unsupported XML feature -LINE 1: SELECT xmlserialize(CONTENT 'text node ... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml 'bar' IS DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT xml 'bar' IS DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml 'barfoo' IS DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT xml 'barfoo' IS DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml '' IS NOT DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT xml '' IS NOT DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml 'abc' IS NOT DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT xml 'abc' IS NOT DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT '<>' IS NOT DOCUMENT; -ERROR: unsupported XML feature -LINE 1: SELECT '<>' IS NOT DOCUMENT; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlagg(data) FROM xmltest; - xmlagg --------- - -(1 row) - -SELECT xmlagg(data) FROM xmltest WHERE id > 10; - xmlagg --------- - -(1 row) - -SELECT xmlelement(name employees, xmlagg(xmlelement(name name, name))) FROM emp; -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- Check mapping SQL identifier to XML name -SELECT xmlpi(name ":::_xml_abc135.%-&_"); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmlpi(name "123"); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -PREPARE foo (xml) AS SELECT xmlconcat('', $1); -ERROR: unsupported XML feature -LINE 1: PREPARE foo (xml) AS SELECT xmlconcat('', $1); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SET XML OPTION DOCUMENT; -EXECUTE foo (''); -ERROR: prepared statement "foo" does not exist -EXECUTE foo ('bad'); -ERROR: prepared statement "foo" does not exist -SELECT xml ''; -ERROR: unsupported XML feature -LINE 1: SELECT xml ''; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SET XML OPTION CONTENT; -EXECUTE foo (''); -ERROR: prepared statement "foo" does not exist -EXECUTE foo ('good'); -ERROR: prepared statement "foo" does not exist -SELECT xml ' '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ' '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ' '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ''; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml ' oops '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ' oops '; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml ' '; -ERROR: unsupported XML feature -LINE 1: SELECT xml ' '; - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml ''; -ERROR: unsupported XML feature -LINE 1: SELECT xml ''; - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- Test backwards parsing -CREATE VIEW xmlview1 AS SELECT xmlcomment('test'); -CREATE VIEW xmlview2 AS SELECT xmlconcat('hello', 'you'); -ERROR: unsupported XML feature -LINE 1: CREATE VIEW xmlview2 AS SELECT xmlconcat('hello', 'you'); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview3 AS SELECT xmlelement(name element, xmlattributes (1 as ":one:", 'deuce' as two), 'content&'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview4 AS SELECT xmlelement(name employee, xmlforest(name, age, salary as pay)) FROM emp; -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview5 AS SELECT xmlparse(content 'x'); -CREATE VIEW xmlview6 AS SELECT xmlpi(name foo, 'bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview7 AS SELECT xmlroot(xml '', version no value, standalone yes); -ERROR: unsupported XML feature -LINE 1: CREATE VIEW xmlview7 AS SELECT xmlroot(xml '', version... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview8 AS SELECT xmlserialize(content 'good' as char(10)); -ERROR: unsupported XML feature -LINE 1: ...EATE VIEW xmlview8 AS SELECT xmlserialize(content 'good' as ... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview9 AS SELECT xmlserialize(content 'good' as text); -ERROR: unsupported XML feature -LINE 1: ...EATE VIEW xmlview9 AS SELECT xmlserialize(content 'good' as ... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -CREATE VIEW xmlview10 AS SELECT xmlserialize(document '42' AS text indent); -ERROR: unsupported XML feature -LINE 1: ...TE VIEW xmlview10 AS SELECT xmlserialize(document '42' AS character varying no indent); -ERROR: unsupported XML feature -LINE 1: ...TE VIEW xmlview11 AS SELECT xmlserialize(document 'x'::text STRIP WHITESPACE) AS "xmlparse"; -(2 rows) - --- Text XPath expressions evaluation -SELECT xpath('/value', data) FROM xmltest; - xpath -------- -(0 rows) - -SELECT xpath(NULL, NULL) IS NULL FROM xmltest; - ?column? ----------- -(0 rows) - -SELECT xpath('', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('//text()', 'number one'); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//text()', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//loc:piece/@id', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//loc:piece', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//loc:piece', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//@value', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('''<>''', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('''<>''', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('count(//*)', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('count(//*)', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('count(//*)=0', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('count(//*)=0', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('count(//*)=3', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('count(//*)=3', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('name(/*)', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('name(/*)', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('/nosuchtag', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('/nosuchtag', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('root', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('root', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath('//namespace::foo', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('//namespace::foo', ''; - degree_symbol text; - res xml[]; -BEGIN - -- Per the documentation, except when the server encoding is UTF8, xpath() - -- may not work on non-ASCII data. The untranslatable_character and - -- undefined_function traps below, currently dead code, will become relevant - -- if we remove this limitation. - IF current_setting('server_encoding') <> 'UTF8' THEN - RAISE LOG 'skip: encoding % unsupported for xpath', - current_setting('server_encoding'); - RETURN; - END IF; - - degree_symbol := convert_from('\xc2b0', 'UTF8'); - res := xpath('text()', (xml_declaration || - '' || degree_symbol || '')::xml); - IF degree_symbol <> res[1]::text THEN - RAISE 'expected % (%), got % (%)', - degree_symbol, convert_to(degree_symbol, 'UTF8'), - res[1], convert_to(res[1]::text, 'UTF8'); - END IF; -EXCEPTION - -- character with byte sequence 0xc2 0xb0 in encoding "UTF8" has no equivalent in encoding "LATIN8" - WHEN untranslatable_character - -- default conversion function for encoding "UTF8" to "MULE_INTERNAL" does not exist - OR undefined_function - -- unsupported XML feature - OR feature_not_supported THEN - RAISE LOG 'skip: %', SQLERRM; -END -$$; --- Test xmlexists and xpath_exists -SELECT xmlexists('//town[text() = ''Toronto'']' PASSING BY REF 'Bidford-on-AvonCwmbranBristol'); -ERROR: unsupported XML feature -LINE 1: ...sts('//town[text() = ''Toronto'']' PASSING BY REF 'Bidford-on-AvonCwmbranBristol'); -ERROR: unsupported XML feature -LINE 1: ...sts('//town[text() = ''Cwmbran'']' PASSING BY REF ''); -ERROR: unsupported XML feature -LINE 1: ...LECT xmlexists('count(/nosuchtag)' PASSING BY REF '')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xpath_exists('//town[text() = ''Toronto'']','Bidford-on-AvonCwmbranBristol'::xml); -ERROR: unsupported XML feature -LINE 1: ...ELECT xpath_exists('//town[text() = ''Toronto'']','Bidford-on-AvonCwmbranBristol'::xml); -ERROR: unsupported XML feature -LINE 1: ...ELECT xpath_exists('//town[text() = ''Cwmbran'']',''::xml); -ERROR: unsupported XML feature -LINE 1: SELECT xpath_exists('count(/nosuchtag)', ''::xml); - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest VALUES (4, 'BudvarfreeCarlinglots'::xml); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (4, 'BudvarMolsonfreeCarlinglots'::xml); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (5, 'MolsonBudvarfreeCarlinglots'::xml); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (6, 'MolsonfreeCarlinglots'::xml); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest VALUES (7, 'number one'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('bar'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('&'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed(''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed(''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xml_is_well_formed('&idontexist;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SET xmloption TO CONTENT; -SELECT xml_is_well_formed('abc'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- Since xpath() deals with namespaces, it's a bit stricter about --- what's well-formed and what's not. If we don't obey these rules --- (i.e. ignore namespace-related errors from libxml), xpath() --- fails in subtle ways. The following would for example produce --- the xml value --- --- which is invalid because '<' may not appear un-escaped in --- attribute values. --- Since different libxml versions emit slightly different --- error messages, we suppress the DETAIL in this test. -\set VERBOSITY terse -SELECT xpath('/*', ''); -ERROR: unsupported XML feature at character 20 -\set VERBOSITY default --- Again, the XML isn't well-formed for namespace purposes -SELECT xpath('/*', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('/*', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- XPath deprecates relative namespaces, but they're not supposed to --- throw an error, only a warning. -SELECT xpath('/*', ''); -ERROR: unsupported XML feature -LINE 1: SELECT xpath('/*', ''); - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- External entity references should not leak filesystem information. -SELECT XMLPARSE(DOCUMENT ']>&c;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT XMLPARSE(DOCUMENT ']>&c;'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- This might or might not load the requested DTD, but it mustn't throw error. -SELECT XMLPARSE(DOCUMENT ' '); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- XMLPATH tests -CREATE TABLE xmldata(data xml); -INSERT INTO xmldata VALUES(' - - AU - Australia - 3 - - - CN - China - 3 - - - HK - HongKong - 3 - - - IN - India - 3 - - - JP - Japan - 3Sinzo Abe - - - SG - Singapore - 3791 - -'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmldata VALUES(' - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- XMLTABLE with columns -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME/text()' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -CREATE VIEW xmltableview1 AS SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME/text()' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -SELECT * FROM xmltableview1; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -\sv xmltableview1 -CREATE OR REPLACE VIEW public.xmltableview1 AS - SELECT "xmltable".id, - "xmltable"._id, - "xmltable".country_name, - "xmltable".country_id, - "xmltable".region_id, - "xmltable".size, - "xmltable".unit, - "xmltable".premier_name - FROM ( SELECT xmldata.data - FROM xmldata) x, - LATERAL XMLTABLE(('/ROWS/ROW'::text) PASSING (x.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME/text()'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -EXPLAIN (COSTS OFF) SELECT * FROM xmltableview1; - QUERY PLAN ------------------------------------------ - Nested Loop - -> Seq Scan on xmldata - -> Table Function Scan on "xmltable" -(3 rows) - -EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME/text()'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -(7 rows) - --- errors -SELECT * FROM XMLTABLE (ROW () PASSING null COLUMNS v1 timestamp) AS f (v1, v2); -ERROR: XMLTABLE function has 1 columns available but 2 columns specified -SELECT * FROM XMLTABLE (ROW () PASSING null COLUMNS v1 timestamp __pg__is_not_null 1) AS f (v1); -ERROR: option name "__pg__is_not_null" cannot be used in XMLTABLE -LINE 1: ...MLTABLE (ROW () PASSING null COLUMNS v1 timestamp __pg__is_n... - ^ --- XMLNAMESPACES tests -SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), - '/zz:rows/zz:row' - PASSING '10' - COLUMNS a int PATH 'zz:a'); -ERROR: unsupported XML feature -LINE 3: PASSING '10' - COLUMNS a int PATH 'Zz:a'); -ERROR: unsupported XML feature -LINE 3: PASSING '10' - COLUMNS a int PATH 'a'); -ERROR: unsupported XML feature -LINE 3: PASSING '' - COLUMNS a text PATH 'foo/namespace::node()'); -ERROR: unsupported XML feature -LINE 2: PASSING '' - ^ -DETAIL: This functionality requires the server to be built with libxml support. --- used in prepare statements -PREPARE pp AS -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -EXECUTE pp; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int); - COUNTRY_NAME | REGION_ID ---------------+----------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY, "COUNTRY_NAME" text, "REGION_ID" int); - id | COUNTRY_NAME | REGION_ID -----+--------------+----------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int); - id | COUNTRY_NAME | REGION_ID -----+--------------+----------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id'); - id ----- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY); - id ----- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH '.'); - id | COUNTRY_NAME | REGION_ID | rawdata -----+--------------+-----------+--------- -(0 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH './*'); - id | COUNTRY_NAME | REGION_ID | rawdata -----+--------------+-----------+--------- -(0 rows) - -SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM xmltable('/root' passing 'a1aa1aa2a bbbbxxxcccc' COLUMNS element text PATH 'element/text()'); -- should fail -ERROR: unsupported XML feature -LINE 1: SELECT * FROM xmltable('/root' passing 'a1a &"<>!foo]]>2' columns c text); -ERROR: unsupported XML feature -LINE 1: select * from xmltable('d/r' passing ''"&<>' COLUMNS ent text); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM xmltable('/x/a' PASSING '''"&<>' COLUMNS ent xml); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM xmltable('/x/a' PASSING '' Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -(7 rows) - --- test qual -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan'; - COUNTRY_NAME | REGION_ID ---------------+----------- -(0 rows) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT f.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) AS f WHERE "COUNTRY_NAME" = 'Japan'; - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Nested Loop - Output: f."COUNTRY_NAME", f."REGION_ID" - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" f - Output: f."COUNTRY_NAME", f."REGION_ID" - Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer) - Filter: (f."COUNTRY_NAME" = 'Japan'::text) -(8 rows) - -EXPLAIN (VERBOSE, FORMAT JSON, COSTS OFF) -SELECT f.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) AS f WHERE "COUNTRY_NAME" = 'Japan'; - QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - [ + - { + - "Plan": { + - "Node Type": "Nested Loop", + - "Parallel Aware": false, + - "Async Capable": false, + - "Join Type": "Inner", + - "Disabled": false, + - "Output": ["f.\"COUNTRY_NAME\"", "f.\"REGION_ID\""], + - "Inner Unique": false, + - "Plans": [ + - { + - "Node Type": "Seq Scan", + - "Parent Relationship": "Outer", + - "Parallel Aware": false, + - "Async Capable": false, + - "Relation Name": "xmldata", + - "Schema": "public", + - "Alias": "xmldata", + - "Disabled": false, + - "Output": ["xmldata.data"] + - }, + - { + - "Node Type": "Table Function Scan", + - "Parent Relationship": "Inner", + - "Parallel Aware": false, + - "Async Capable": false, + - "Table Function Name": "xmltable", + - "Alias": "f", + - "Disabled": false, + - "Output": ["f.\"COUNTRY_NAME\"", "f.\"REGION_ID\""], + - "Table Function Call": "XMLTABLE(('/ROWS/ROW[COUNTRY_NAME=\"Japan\" or COUNTRY_NAME=\"India\"]'::text) PASSING (xmldata.data) COLUMNS \"COUNTRY_NAME\" text, \"REGION_ID\" integer)",+ - "Filter": "(f.\"COUNTRY_NAME\" = 'Japan'::text)" + - } + - ] + - } + - } + - ] -(1 row) - --- should to work with more data -INSERT INTO xmldata VALUES(' - - CZ - Czech Republic - 2Milos Zeman - - - DE - Germany - 2 - - - FR - France - 2 - -'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmldata VALUES(' - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmldata VALUES(' - - EG - Egypt - 1 - - - SD - Sudan - 1 - -'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmldata VALUES(' - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') - WHERE region_id = 2; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') - WHERE region_id = 2; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) - Filter: ("xmltable".region_id = 2) -(8 rows) - --- should fail, NULL value -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE' NOT NULL, - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+-------------- -(0 rows) - --- if all is ok, then result is empty --- one line xml test -WITH - x AS (SELECT proname, proowner, procost::numeric, pronargs, - array_to_string(proargnames,',') as proargnames, - case when proargtypes <> '' then array_to_string(proargtypes::oid[],',') end as proargtypes - FROM pg_proc WHERE proname = 'f_leak'), - y AS (SELECT xmlelement(name proc, - xmlforest(proname, proowner, - procost, pronargs, - proargnames, proargtypes)) as proc - FROM x), - z AS (SELECT xmltable.* - FROM y, - LATERAL xmltable('/proc' PASSING proc - COLUMNS proname name, - proowner oid, - procost float, - pronargs int, - proargnames text, - proargtypes text)) - SELECT * FROM z - EXCEPT SELECT * FROM x; -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. --- multi line xml test, result should be empty too -WITH - x AS (SELECT proname, proowner, procost::numeric, pronargs, - array_to_string(proargnames,',') as proargnames, - case when proargtypes <> '' then array_to_string(proargtypes::oid[],',') end as proargtypes - FROM pg_proc), - y AS (SELECT xmlelement(name data, - xmlagg(xmlelement(name proc, - xmlforest(proname, proowner, procost, - pronargs, proargnames, proargtypes)))) as doc - FROM x), - z AS (SELECT xmltable.* - FROM y, - LATERAL xmltable('/data/proc' PASSING doc - COLUMNS proname name, - proowner oid, - procost float, - pronargs int, - proargnames text, - proargtypes text)) - SELECT * FROM z - EXCEPT SELECT * FROM x; -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -CREATE TABLE xmltest2(x xml, _path text); -INSERT INTO xmltest2 VALUES('1', 'A'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest2 VALUES('1', 'A')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest2 VALUES('2', 'B'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest2 VALUES('2', 'B')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest2 VALUES('3', 'C'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest2 VALUES('3', 'C')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -INSERT INTO xmltest2 VALUES('2', 'D'); -ERROR: unsupported XML feature -LINE 1: INSERT INTO xmltest2 VALUES('2', 'D')... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltable.* FROM xmltest2, LATERAL xmltable('/d/r' PASSING x COLUMNS a int PATH '' || lower(_path) || 'c'); - a ---- -(0 rows) - -SELECT xmltable.* FROM xmltest2, LATERAL xmltable(('/d/r/' || lower(_path) || 'c') PASSING x COLUMNS a int PATH '.'); - a ---- -(0 rows) - -SELECT xmltable.* FROM xmltest2, LATERAL xmltable(('/d/r/' || lower(_path) || 'c') PASSING x COLUMNS a int PATH 'x' DEFAULT ascii(_path) - 54); - a ---- -(0 rows) - --- XPath result can be boolean or number too -SELECT * FROM XMLTABLE('*' PASSING 'a' COLUMNS a xml PATH '.', b text PATH '.', c text PATH '"hi"', d boolean PATH '. = "a"', e integer PATH 'string-length(.)'); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM XMLTABLE('*' PASSING 'a' COLUMNS a xml ... - ^ -DETAIL: This functionality requires the server to be built with libxml support. -\x -SELECT * FROM XMLTABLE('*' PASSING 'pre&deeppost' COLUMNS x xml PATH '/e/n2', y xml PATH '/'); -ERROR: unsupported XML feature -LINE 1: SELECT * FROM XMLTABLE('*' PASSING 'pre"', b xml PATH '""'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext(NULL); - xmltext ---------- - -(1 row) - -SELECT xmltext(''); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext(' '); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext('foo & <"bar">'); -ERROR: unsupported XML feature -DETAIL: This functionality requires the server to be built with libxml support. -SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j'::char); -ERROR: unsupported XML feature -LINE 1: SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j':... - ^ -DETAIL: This functionality requires the server to be built with libxml support. +-- If no XML data could be inserted, skip the tests as the server has been +-- compiled without libxml support. +SELECT count(*) = 0 AS skip_test FROM xmltest \gset +\if :skip_test +\quit diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out deleted file mode 100644 index e0ae6395122..00000000000 --- a/src/test/regress/expected/xml_2.out +++ /dev/null @@ -1,1875 +0,0 @@ -CREATE TABLE xmltest ( - id int, - data xml -); -INSERT INTO xmltest VALUES (1, 'one'); -INSERT INTO xmltest VALUES (2, 'two'); -INSERT INTO xmltest VALUES (3, 'one - 2 | two -(2 rows) - --- test non-throwing API, too -SELECT pg_input_is_valid('one', 'xml'); - pg_input_is_valid -------------------- - t -(1 row) - -SELECT pg_input_is_valid('oneone', 'xml'); - pg_input_is_valid -------------------- - f -(1 row) - -SELECT message FROM pg_input_error_info('', 'xml'); - message ----------------------------------------------- - invalid XML content: invalid XML declaration -(1 row) - -SELECT xmlcomment('test'); - xmlcomment -------------- - -(1 row) - -SELECT xmlcomment('-test'); - xmlcomment --------------- - -(1 row) - -SELECT xmlcomment('test-'); -ERROR: invalid XML comment -SELECT xmlcomment('--test'); -ERROR: invalid XML comment -SELECT xmlcomment('te st'); - xmlcomment --------------- - -(1 row) - -SELECT xmlconcat(xmlcomment('hello'), - xmlelement(NAME qux, 'foo'), - xmlcomment('world')); - xmlconcat ----------------------------------------- - foo -(1 row) - -SELECT xmlconcat('hello', 'you'); - xmlconcat ------------ - helloyou -(1 row) - -SELECT xmlconcat(1, 2); -ERROR: argument of XMLCONCAT must be type xml, not type integer -LINE 1: SELECT xmlconcat(1, 2); - ^ -SELECT xmlconcat('bad', '', NULL, ''); - xmlconcat --------------- - -(1 row) - -SELECT xmlconcat('', NULL, ''); - xmlconcat ------------------------------------ - -(1 row) - -SELECT xmlconcat(NULL); - xmlconcat ------------ - -(1 row) - -SELECT xmlconcat(NULL, NULL); - xmlconcat ------------ - -(1 row) - -SELECT xmlelement(name element, - xmlattributes (1 as one, 'deuce' as two), - 'content'); - xmlelement ------------------------------------------------- - content -(1 row) - -SELECT xmlelement(name element, - xmlattributes ('unnamed and wrong')); -ERROR: unnamed XML attribute value must be a column reference -LINE 2: xmlattributes ('unnamed and wrong')); - ^ -SELECT xmlelement(name element, xmlelement(name nested, 'stuff')); - xmlelement -------------------------------------------- - stuff -(1 row) - -SELECT xmlelement(name employee, xmlforest(name, age, salary as pay)) FROM emp; - xmlelement ----------------------------------------------------------------------- - sharon251000 - sam302000 - bill201000 - jeff23600 - cim30400 - linda19100 -(6 rows) - -SELECT xmlelement(name duplicate, xmlattributes(1 as a, 2 as b, 3 as a)); -ERROR: XML attribute name "a" appears more than once -LINE 1: ...ment(name duplicate, xmlattributes(1 as a, 2 as b, 3 as a)); - ^ -SELECT xmlelement(name num, 37); - xmlelement ---------------- - 37 -(1 row) - -SELECT xmlelement(name foo, text 'bar'); - xmlelement ----------------- - bar -(1 row) - -SELECT xmlelement(name foo, xml 'bar'); - xmlelement ----------------- - bar -(1 row) - -SELECT xmlelement(name foo, text 'br'); - xmlelement -------------------------- - b<a/>r -(1 row) - -SELECT xmlelement(name foo, xml 'br'); - xmlelement -------------------- - br -(1 row) - -SELECT xmlelement(name foo, array[1, 2, 3]); - xmlelement -------------------------------------------------------------------------- - 123 -(1 row) - -SET xmlbinary TO base64; -SELECT xmlelement(name foo, bytea 'bar'); - xmlelement ------------------ - YmFy -(1 row) - -SET xmlbinary TO hex; -SELECT xmlelement(name foo, bytea 'bar'); - xmlelement -------------------- - 626172 -(1 row) - -SELECT xmlelement(name foo, xmlattributes(true as bar)); - xmlelement -------------------- - -(1 row) - -SELECT xmlelement(name foo, xmlattributes('2009-04-09 00:24:37'::timestamp as bar)); - xmlelement ----------------------------------- - -(1 row) - -SELECT xmlelement(name foo, xmlattributes('infinity'::timestamp as bar)); -ERROR: timestamp out of range -DETAIL: XML does not support infinite timestamp values. -SELECT xmlelement(name foo, xmlattributes('<>&"''' as funny, xml 'br' as funnier)); - xmlelement ------------------------------------------------------------- - -(1 row) - -SELECT xmlparse(content ''); - xmlparse ----------- - -(1 row) - -SELECT xmlparse(content ' '); - xmlparse ----------- - -(1 row) - -SELECT xmlparse(content 'abc'); - xmlparse ----------- - abc -(1 row) - -SELECT xmlparse(content 'x'); - xmlparse --------------- - x -(1 row) - -SELECT xmlparse(content '&'); -ERROR: invalid XML content -DETAIL: line 1: xmlParseEntityRef: no name -& - ^ -SELECT xmlparse(content '&idontexist;'); -ERROR: invalid XML content -DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; - ^ -SELECT xmlparse(content ''); - xmlparse ---------------------------- - -(1 row) - -SELECT xmlparse(content ''); - xmlparse --------------------------------- - -(1 row) - -SELECT xmlparse(content '&idontexist;'); -ERROR: invalid XML content -DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; - ^ -line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced -SELECT xmlparse(content ''); - xmlparse ---------------------- - -(1 row) - -SELECT xmlparse(document ' '); -ERROR: invalid XML document -DETAIL: line 1: Start tag expected, '<' not found -SELECT xmlparse(document 'abc'); -ERROR: invalid XML document -DETAIL: line 1: Start tag expected, '<' not found -abc -^ -SELECT xmlparse(document 'x'); - xmlparse --------------- - x -(1 row) - -SELECT xmlparse(document '&'); -ERROR: invalid XML document -DETAIL: line 1: xmlParseEntityRef: no name -& - ^ -line 1: Opening and ending tag mismatch: invalidentity line 1 and abc -SELECT xmlparse(document '&idontexist;'); -ERROR: invalid XML document -DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; - ^ -line 1: Opening and ending tag mismatch: undefinedentity line 1 and abc -SELECT xmlparse(document ''); - xmlparse ---------------------------- - -(1 row) - -SELECT xmlparse(document ''); - xmlparse --------------------------------- - -(1 row) - -SELECT xmlparse(document '&idontexist;'); -ERROR: invalid XML document -DETAIL: line 1: Entity 'idontexist' not defined -&idontexist; - ^ -line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced -SELECT xmlparse(document ''); - xmlparse ---------------------- - -(1 row) - -SELECT xmlpi(name foo); - xmlpi ---------- - -(1 row) - -SELECT xmlpi(name xml); -ERROR: invalid XML processing instruction -DETAIL: XML processing instruction target name cannot be "xml". -SELECT xmlpi(name xmlstuff); - xmlpi --------------- - -(1 row) - -SELECT xmlpi(name foo, 'bar'); - xmlpi -------------- - -(1 row) - -SELECT xmlpi(name foo, 'in?>valid'); -ERROR: invalid XML processing instruction -DETAIL: XML processing instruction cannot contain "?>". -SELECT xmlpi(name foo, null); - xmlpi -------- - -(1 row) - -SELECT xmlpi(name xml, null); -ERROR: invalid XML processing instruction -DETAIL: XML processing instruction target name cannot be "xml". -SELECT xmlpi(name xmlstuff, null); - xmlpi -------- - -(1 row) - -SELECT xmlpi(name "xml-stylesheet", 'href="mystyle.css" type="text/css"'); - xmlpi -------------------------------------------------------- - -(1 row) - -SELECT xmlpi(name foo, ' bar'); - xmlpi -------------- - -(1 row) - -SELECT xmlroot(xml '', version no value, standalone no value); - xmlroot ---------- - -(1 row) - -SELECT xmlroot(xml '', version '2.0'); - xmlroot ------------------------------ - -(1 row) - -SELECT xmlroot(xml '', version no value, standalone yes); - xmlroot ----------------------------------------------- - -(1 row) - -SELECT xmlroot(xml '', version no value, standalone yes); - xmlroot ----------------------------------------------- - -(1 row) - -SELECT xmlroot(xmlroot(xml '', version '1.0'), version '1.1', standalone no); - xmlroot ---------------------------------------------- - -(1 row) - -SELECT xmlroot('', version no value, standalone no); - xmlroot ---------------------------------------------- - -(1 row) - -SELECT xmlroot('', version no value, standalone no value); - xmlroot ---------- - -(1 row) - -SELECT xmlroot('', version no value); - xmlroot ----------------------------------------------- - -(1 row) - -SELECT xmlroot ( - xmlelement ( - name gazonk, - xmlattributes ( - 'val' AS name, - 1 + 1 AS num - ), - xmlelement ( - NAME qux, - 'foo' - ) - ), - version '1.0', - standalone yes -); - xmlroot ------------------------------------------------------------------------------------------- - foo -(1 row) - -SELECT xmlserialize(content data as character varying(20)) FROM xmltest; - xmlserialize --------------------- - one - two -(2 rows) - -SELECT xmlserialize(content 'good' as char(10)); - xmlserialize --------------- - good -(1 row) - -SELECT xmlserialize(document 'bad' as text); -ERROR: not an XML document --- indent -SELECT xmlserialize(DOCUMENT '42' AS text INDENT); - xmlserialize -------------------------- - + - + - 42+ - + - -(1 row) - -SELECT xmlserialize(CONTENT '42' AS text INDENT); - xmlserialize -------------------------- - + - + - 42+ - + - -(1 row) - --- no indent -SELECT xmlserialize(DOCUMENT '42' AS text NO INDENT); - xmlserialize -------------------------------------------- - 42 -(1 row) - -SELECT xmlserialize(CONTENT '42' AS text NO INDENT); - xmlserialize -------------------------------------------- - 42 -(1 row) - --- indent non singly-rooted xml -SELECT xmlserialize(DOCUMENT '7342' AS text INDENT); -ERROR: not an XML document -SELECT xmlserialize(CONTENT '7342' AS text INDENT); - xmlserialize ------------------------ - 73 + - + - 42+ - -(1 row) - --- indent non singly-rooted xml with mixed contents -SELECT xmlserialize(DOCUMENT 'text node73text node42' AS text INDENT); -ERROR: not an XML document -SELECT xmlserialize(CONTENT 'text node73text node42' AS text INDENT); - xmlserialize ------------------------- - text node + - 73text node+ - + - 42 + - -(1 row) - --- indent singly-rooted xml with mixed contents -SELECT xmlserialize(DOCUMENT '42text node73' AS text INDENT); - xmlserialize ---------------------------------------------- - + - + - 42 + - text node73+ - + - -(1 row) - -SELECT xmlserialize(CONTENT '42text node73' AS text INDENT); - xmlserialize ---------------------------------------------- - + - + - 42 + - text node73+ - + - -(1 row) - --- indent empty string -SELECT xmlserialize(DOCUMENT '' AS text INDENT); -ERROR: not an XML document -SELECT xmlserialize(CONTENT '' AS text INDENT); - xmlserialize --------------- - -(1 row) - --- whitespaces -SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); -ERROR: not an XML document -SELECT xmlserialize(CONTENT ' ' AS text INDENT); - xmlserialize --------------- - -(1 row) - --- indent null -SELECT xmlserialize(DOCUMENT NULL AS text INDENT); - xmlserialize --------------- - -(1 row) - -SELECT xmlserialize(CONTENT NULL AS text INDENT); - xmlserialize --------------- - -(1 row) - --- indent with XML declaration -SELECT xmlserialize(DOCUMENT '73' AS text INDENT); - xmlserialize ----------------------------------------- - + - + - + - 73 + - + - -(1 row) - -SELECT xmlserialize(CONTENT '73' AS text INDENT); - xmlserialize -------------------- - + - + - 73+ - + - -(1 row) - --- indent containing DOCTYPE declaration -SELECT xmlserialize(DOCUMENT '' AS text INDENT); - xmlserialize --------------- - + - -(1 row) - -SELECT xmlserialize(CONTENT '' AS text INDENT); - xmlserialize --------------- - + - + - -(1 row) - --- indent xml with empty element -SELECT xmlserialize(DOCUMENT '' AS text INDENT); - xmlserialize --------------- - + - + - -(1 row) - -SELECT xmlserialize(CONTENT '' AS text INDENT); - xmlserialize --------------- - + - + - -(1 row) - --- 'no indent' = not using 'no indent' -SELECT xmlserialize(DOCUMENT '42' AS text) = xmlserialize(DOCUMENT '42' AS text NO INDENT); - ?column? ----------- - t -(1 row) - -SELECT xmlserialize(CONTENT '42' AS text) = xmlserialize(CONTENT '42' AS text NO INDENT); - ?column? ----------- - t -(1 row) - --- indent xml strings containing blank nodes -SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); - xmlserialize --------------- - + - + - -(1 row) - -SELECT xmlserialize(CONTENT 'text node ' AS text INDENT); - xmlserialize --------------- - text node + - + - + - -(1 row) - -SELECT xml 'bar' IS DOCUMENT; - ?column? ----------- - t -(1 row) - -SELECT xml 'barfoo' IS DOCUMENT; - ?column? ----------- - f -(1 row) - -SELECT xml '' IS NOT DOCUMENT; - ?column? ----------- - f -(1 row) - -SELECT xml 'abc' IS NOT DOCUMENT; - ?column? ----------- - t -(1 row) - -SELECT '<>' IS NOT DOCUMENT; -ERROR: invalid XML content -LINE 1: SELECT '<>' IS NOT DOCUMENT; - ^ -DETAIL: line 1: StartTag: invalid element name -<> - ^ -SELECT xmlagg(data) FROM xmltest; - xmlagg --------------------------------------- - onetwo -(1 row) - -SELECT xmlagg(data) FROM xmltest WHERE id > 10; - xmlagg --------- - -(1 row) - -SELECT xmlelement(name employees, xmlagg(xmlelement(name name, name))) FROM emp; - xmlelement --------------------------------------------------------------------------------------------------------------------------------- - sharonsambilljeffcimlinda -(1 row) - --- Check mapping SQL identifier to XML name -SELECT xmlpi(name ":::_xml_abc135.%-&_"); - xmlpi -------------------------------------------------- - -(1 row) - -SELECT xmlpi(name "123"); - xmlpi ---------------- - -(1 row) - -PREPARE foo (xml) AS SELECT xmlconcat('', $1); -SET XML OPTION DOCUMENT; -EXECUTE foo (''); - xmlconcat --------------- - -(1 row) - -EXECUTE foo ('bad'); -ERROR: invalid XML document -LINE 1: EXECUTE foo ('bad'); - ^ -DETAIL: line 1: Start tag expected, '<' not found -bad -^ -SELECT xml ''; -ERROR: invalid XML document -LINE 1: SELECT xml ''; - ^ -DETAIL: line 1: Extra content at the end of the document - - ^ -SET XML OPTION CONTENT; -EXECUTE foo (''); - xmlconcat --------------- - -(1 row) - -EXECUTE foo ('good'); - xmlconcat ------------- - good -(1 row) - -SELECT xml ' '; - xml --------------------------------------------------------------------- - -(1 row) - -SELECT xml ' '; - xml ------------------------------- - -(1 row) - -SELECT xml ''; - xml ------------------- - -(1 row) - -SELECT xml ' oops '; -ERROR: invalid XML content -LINE 1: SELECT xml ' oops '; - ^ -DETAIL: line 1: StartTag: invalid element name - oops - ^ -SELECT xml ' '; -ERROR: invalid XML content -LINE 1: SELECT xml ' '; - ^ -DETAIL: line 1: StartTag: invalid element name - - ^ -SELECT xml ''; -ERROR: invalid XML content -LINE 1: SELECT xml ''; - ^ -DETAIL: line 1: Extra content at the end of the document - - ^ --- Test backwards parsing -CREATE VIEW xmlview1 AS SELECT xmlcomment('test'); -CREATE VIEW xmlview2 AS SELECT xmlconcat('hello', 'you'); -CREATE VIEW xmlview3 AS SELECT xmlelement(name element, xmlattributes (1 as ":one:", 'deuce' as two), 'content&'); -CREATE VIEW xmlview4 AS SELECT xmlelement(name employee, xmlforest(name, age, salary as pay)) FROM emp; -CREATE VIEW xmlview5 AS SELECT xmlparse(content 'x'); -CREATE VIEW xmlview6 AS SELECT xmlpi(name foo, 'bar'); -CREATE VIEW xmlview7 AS SELECT xmlroot(xml '', version no value, standalone yes); -CREATE VIEW xmlview8 AS SELECT xmlserialize(content 'good' as char(10)); -CREATE VIEW xmlview9 AS SELECT xmlserialize(content 'good' as text); -CREATE VIEW xmlview10 AS SELECT xmlserialize(document '42' AS text indent); -CREATE VIEW xmlview11 AS SELECT xmlserialize(document '42' AS character varying no indent); -SELECT table_name, view_definition FROM information_schema.views - WHERE table_name LIKE 'xmlview%' ORDER BY 1; - table_name | view_definition -------------+--------------------------------------------------------------------------------------------------------------------------------------- - xmlview1 | SELECT xmlcomment('test'::text) AS xmlcomment; - xmlview10 | SELECT XMLSERIALIZE(DOCUMENT '42'::xml AS text INDENT) AS "xmlserialize"; - xmlview11 | SELECT (XMLSERIALIZE(DOCUMENT '42'::xml AS character varying NO INDENT))::character varying AS "xmlserialize"; - xmlview2 | SELECT XMLCONCAT('hello'::xml, 'you'::xml) AS "xmlconcat"; - xmlview3 | SELECT XMLELEMENT(NAME element, XMLATTRIBUTES(1 AS ":one:", 'deuce' AS two), 'content&') AS "xmlelement"; - xmlview4 | SELECT XMLELEMENT(NAME employee, XMLFOREST(name AS name, age AS age, salary AS pay)) AS "xmlelement" + - | FROM emp; - xmlview5 | SELECT XMLPARSE(CONTENT 'x'::text STRIP WHITESPACE) AS "xmlparse"; - xmlview6 | SELECT XMLPI(NAME foo, 'bar'::text) AS "xmlpi"; - xmlview7 | SELECT XMLROOT(''::xml, VERSION NO VALUE, STANDALONE YES) AS "xmlroot"; - xmlview8 | SELECT (XMLSERIALIZE(CONTENT 'good'::xml AS character(10) NO INDENT))::character(10) AS "xmlserialize"; - xmlview9 | SELECT XMLSERIALIZE(CONTENT 'good'::xml AS text NO INDENT) AS "xmlserialize"; -(11 rows) - --- Text XPath expressions evaluation -SELECT xpath('/value', data) FROM xmltest; - xpath ----------------------- - {one} - {two} -(2 rows) - -SELECT xpath(NULL, NULL) IS NULL FROM xmltest; - ?column? ----------- - t - t -(2 rows) - -SELECT xpath('', ''); -ERROR: empty XPath expression -CONTEXT: SQL function "xpath" statement 1 -SELECT xpath('//text()', 'number one'); - xpath ----------------- - {"number one"} -(1 row) - -SELECT xpath('//loc:piece/@id', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); - xpath -------- - {1,2} -(1 row) - -SELECT xpath('//loc:piece', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); - xpath ------------------------------------------------------------------------------------------------------------------------------------------------- - {"number one",""} -(1 row) - -SELECT xpath('//loc:piece', 'number one', ARRAY[ARRAY['loc', 'http://127.0.0.1']]); - xpath ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - {"number one",""} -(1 row) - -SELECT xpath('//b', 'one two three etc'); - xpath -------------------------- - {two,etc} -(1 row) - -SELECT xpath('//text()', '<'); - xpath --------- - {<} -(1 row) - -SELECT xpath('//@value', ''); - xpath --------- - {<} -(1 row) - -SELECT xpath('''<>''', ''); - xpath ---------------------------- - {<<invalid>>} -(1 row) - -SELECT xpath('count(//*)', ''); - xpath -------- - {3} -(1 row) - -SELECT xpath('count(//*)=0', ''); - xpath ---------- - {false} -(1 row) - -SELECT xpath('count(//*)=3', ''); - xpath --------- - {true} -(1 row) - -SELECT xpath('name(/*)', ''); - xpath --------- - {root} -(1 row) - -SELECT xpath('/nosuchtag', ''); - xpath -------- - {} -(1 row) - -SELECT xpath('root', ''); - xpath ------------ - {} -(1 row) - -SELECT xpath('//namespace::foo', ''); - xpath --------------------- - {http://127.0.0.1} -(1 row) - --- Round-trip non-ASCII data through xpath(). -DO $$ -DECLARE - xml_declaration text := ''; - degree_symbol text; - res xml[]; -BEGIN - -- Per the documentation, except when the server encoding is UTF8, xpath() - -- may not work on non-ASCII data. The untranslatable_character and - -- undefined_function traps below, currently dead code, will become relevant - -- if we remove this limitation. - IF current_setting('server_encoding') <> 'UTF8' THEN - RAISE LOG 'skip: encoding % unsupported for xpath', - current_setting('server_encoding'); - RETURN; - END IF; - - degree_symbol := convert_from('\xc2b0', 'UTF8'); - res := xpath('text()', (xml_declaration || - '' || degree_symbol || '')::xml); - IF degree_symbol <> res[1]::text THEN - RAISE 'expected % (%), got % (%)', - degree_symbol, convert_to(degree_symbol, 'UTF8'), - res[1], convert_to(res[1]::text, 'UTF8'); - END IF; -EXCEPTION - -- character with byte sequence 0xc2 0xb0 in encoding "UTF8" has no equivalent in encoding "LATIN8" - WHEN untranslatable_character - -- default conversion function for encoding "UTF8" to "MULE_INTERNAL" does not exist - OR undefined_function - -- unsupported XML feature - OR feature_not_supported THEN - RAISE LOG 'skip: %', SQLERRM; -END -$$; --- Test xmlexists and xpath_exists -SELECT xmlexists('//town[text() = ''Toronto'']' PASSING BY REF 'Bidford-on-AvonCwmbranBristol'); - xmlexists ------------ - f -(1 row) - -SELECT xmlexists('//town[text() = ''Cwmbran'']' PASSING BY REF 'Bidford-on-AvonCwmbranBristol'); - xmlexists ------------ - t -(1 row) - -SELECT xmlexists('count(/nosuchtag)' PASSING BY REF ''); - xmlexists ------------ - t -(1 row) - -SELECT xpath_exists('//town[text() = ''Toronto'']','Bidford-on-AvonCwmbranBristol'::xml); - xpath_exists --------------- - f -(1 row) - -SELECT xpath_exists('//town[text() = ''Cwmbran'']','Bidford-on-AvonCwmbranBristol'::xml); - xpath_exists --------------- - t -(1 row) - -SELECT xpath_exists('count(/nosuchtag)', ''::xml); - xpath_exists --------------- - t -(1 row) - -INSERT INTO xmltest VALUES (4, 'BudvarfreeCarlinglots'::xml); -INSERT INTO xmltest VALUES (5, 'MolsonfreeCarlinglots'::xml); -INSERT INTO xmltest VALUES (6, 'BudvarfreeCarlinglots'::xml); -INSERT INTO xmltest VALUES (7, 'MolsonfreeCarlinglots'::xml); -SELECT COUNT(id) FROM xmltest WHERE xmlexists('/menu/beer' PASSING data); - count -------- - 0 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xmlexists('/menu/beer' PASSING BY REF data BY REF); - count -------- - 0 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xmlexists('/menu/beers' PASSING BY REF data); - count -------- - 2 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xmlexists('/menu/beers/name[text() = ''Molson'']' PASSING BY REF data); - count -------- - 1 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/menu/beer',data); - count -------- - 0 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/menu/beers',data); - count -------- - 2 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/menu/beers/name[text() = ''Molson'']',data); - count -------- - 1 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/myns:menu/myns:beer',data,ARRAY[ARRAY['myns','http://myns.com']]); - count -------- - 0 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/myns:menu/myns:beers',data,ARRAY[ARRAY['myns','http://myns.com']]); - count -------- - 2 -(1 row) - -SELECT COUNT(id) FROM xmltest WHERE xpath_exists('/myns:menu/myns:beers/myns:name[text() = ''Molson'']',data,ARRAY[ARRAY['myns','http://myns.com']]); - count -------- - 1 -(1 row) - -CREATE TABLE query ( expr TEXT ); -INSERT INTO query VALUES ('/menu/beers/cost[text() = ''lots'']'); -SELECT COUNT(id) FROM xmltest, query WHERE xmlexists(expr PASSING BY REF data); - count -------- - 2 -(1 row) - --- Test xml_is_well_formed and variants -SELECT xml_is_well_formed_document('bar'); - xml_is_well_formed_document ------------------------------ - t -(1 row) - -SELECT xml_is_well_formed_document('abc'); - xml_is_well_formed_document ------------------------------ - f -(1 row) - -SELECT xml_is_well_formed_content('bar'); - xml_is_well_formed_content ----------------------------- - t -(1 row) - -SELECT xml_is_well_formed_content('abc'); - xml_is_well_formed_content ----------------------------- - t -(1 row) - -SET xmloption TO DOCUMENT; -SELECT xml_is_well_formed('abc'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed('<>'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed(''); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('bar'); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('barbaz'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed('number one'); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('bar'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed('bar'); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('&'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed('&idontexist;'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed(''); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed(''); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed('&idontexist;'); - xml_is_well_formed --------------------- - f -(1 row) - -SET xmloption TO CONTENT; -SELECT xml_is_well_formed('abc'); - xml_is_well_formed --------------------- - t -(1 row) - --- Since xpath() deals with namespaces, it's a bit stricter about --- what's well-formed and what's not. If we don't obey these rules --- (i.e. ignore namespace-related errors from libxml), xpath() --- fails in subtle ways. The following would for example produce --- the xml value --- --- which is invalid because '<' may not appear un-escaped in --- attribute values. --- Since different libxml versions emit slightly different --- error messages, we suppress the DETAIL in this test. -\set VERBOSITY terse -SELECT xpath('/*', ''); -ERROR: could not parse XML document -\set VERBOSITY default --- Again, the XML isn't well-formed for namespace purposes -SELECT xpath('/*', ''); -ERROR: could not parse XML document -DETAIL: line 1: Namespace prefix nosuchprefix on tag is not defined - - ^ -CONTEXT: SQL function "xpath" statement 1 --- XPath deprecates relative namespaces, but they're not supposed to --- throw an error, only a warning. -SELECT xpath('/*', ''); -WARNING: line 1: xmlns: URI relative is not absolute - - ^ - xpath --------------------------------------- - {""} -(1 row) - --- External entity references should not leak filesystem information. -SELECT XMLPARSE(DOCUMENT ']>&c;'); - xmlparse ------------------------------------------------------------------ - ]>&c; -(1 row) - -SELECT XMLPARSE(DOCUMENT ']>&c;'); - xmlparse ------------------------------------------------------------------------ - ]>&c; -(1 row) - --- This might or might not load the requested DTD, but it mustn't throw error. -SELECT XMLPARSE(DOCUMENT ' '); - xmlparse ------------------------------------------------------------------------------------------------------------------------------------------------------- -   -(1 row) - --- XMLPATH tests -CREATE TABLE xmldata(data xml); -INSERT INTO xmldata VALUES(' - - AU - Australia - 3 - - - CN - China - 3 - - - HK - HongKong - 3 - - - IN - India - 3 - - - JP - Japan - 3Sinzo Abe - - - SG - Singapore - 3791 - -'); --- XMLTABLE with columns -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME/text()' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+--------------- - 1 | 1 | Australia | AU | 3 | | | not specified - 2 | 2 | China | CN | 3 | | | not specified - 3 | 3 | HongKong | HK | 3 | | | not specified - 4 | 4 | India | IN | 3 | | | not specified - 5 | 5 | Japan | JP | 3 | | | Sinzo Abe - 6 | 6 | Singapore | SG | 3 | 791 | km | not specified -(6 rows) - -CREATE VIEW xmltableview1 AS SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME/text()' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -SELECT * FROM xmltableview1; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+--------------- - 1 | 1 | Australia | AU | 3 | | | not specified - 2 | 2 | China | CN | 3 | | | not specified - 3 | 3 | HongKong | HK | 3 | | | not specified - 4 | 4 | India | IN | 3 | | | not specified - 5 | 5 | Japan | JP | 3 | | | Sinzo Abe - 6 | 6 | Singapore | SG | 3 | 791 | km | not specified -(6 rows) - -\sv xmltableview1 -CREATE OR REPLACE VIEW public.xmltableview1 AS - SELECT "xmltable".id, - "xmltable"._id, - "xmltable".country_name, - "xmltable".country_id, - "xmltable".region_id, - "xmltable".size, - "xmltable".unit, - "xmltable".premier_name - FROM ( SELECT xmldata.data - FROM xmldata) x, - LATERAL XMLTABLE(('/ROWS/ROW'::text) PASSING (x.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME/text()'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -EXPLAIN (COSTS OFF) SELECT * FROM xmltableview1; - QUERY PLAN ------------------------------------------ - Nested Loop - -> Seq Scan on xmldata - -> Table Function Scan on "xmltable" -(3 rows) - -EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME/text()'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -(7 rows) - --- errors -SELECT * FROM XMLTABLE (ROW () PASSING null COLUMNS v1 timestamp) AS f (v1, v2); -ERROR: XMLTABLE function has 1 columns available but 2 columns specified -SELECT * FROM XMLTABLE (ROW () PASSING null COLUMNS v1 timestamp __pg__is_not_null 1) AS f (v1); -ERROR: option name "__pg__is_not_null" cannot be used in XMLTABLE -LINE 1: ...MLTABLE (ROW () PASSING null COLUMNS v1 timestamp __pg__is_n... - ^ --- XMLNAMESPACES tests -SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), - '/zz:rows/zz:row' - PASSING '10' - COLUMNS a int PATH 'zz:a'); - a ----- - 10 -(1 row) - -CREATE VIEW xmltableview2 AS SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS "Zz"), - '/Zz:rows/Zz:row' - PASSING '10' - COLUMNS a int PATH 'Zz:a'); -SELECT * FROM xmltableview2; - a ----- - 10 -(1 row) - -\sv xmltableview2 -CREATE OR REPLACE VIEW public.xmltableview2 AS - SELECT a - FROM XMLTABLE(XMLNAMESPACES ('http://x.y'::text AS "Zz"), ('/Zz:rows/Zz:row'::text) PASSING ('10'::xml) COLUMNS a integer PATH ('Zz:a'::text)) -SELECT * FROM XMLTABLE(XMLNAMESPACES(DEFAULT 'http://x.y'), - '/rows/row' - PASSING '10' - COLUMNS a int PATH 'a'); -ERROR: DEFAULT namespace is not supported -SELECT * FROM XMLTABLE('.' - PASSING '' - COLUMNS a text PATH 'foo/namespace::node()'); - a --------------------------------------- - http://www.w3.org/XML/1998/namespace -(1 row) - --- used in prepare statements -PREPARE pp AS -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -EXECUTE pp; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+--------------+------------+-----------+------+------+--------------- - 1 | 1 | Australia | AU | 3 | | | not specified - 2 | 2 | China | CN | 3 | | | not specified - 3 | 3 | HongKong | HK | 3 | | | not specified - 4 | 4 | India | IN | 3 | | | not specified - 5 | 5 | Japan | JP | 3 | | | Sinzo Abe - 6 | 6 | Singapore | SG | 3 | 791 | km | not specified -(6 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int); - COUNTRY_NAME | REGION_ID ---------------+----------- - India | 3 - Japan | 3 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY, "COUNTRY_NAME" text, "REGION_ID" int); - id | COUNTRY_NAME | REGION_ID -----+--------------+----------- - 1 | India | 3 - 2 | Japan | 3 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int); - id | COUNTRY_NAME | REGION_ID -----+--------------+----------- - 4 | India | 3 - 5 | Japan | 3 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id'); - id ----- - 4 - 5 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY); - id ----- - 1 - 2 -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH '.'); - id | COUNTRY_NAME | REGION_ID | rawdata -----+--------------+-----------+------------------------------------------------------------------ - 4 | India | 3 | + - | | | IN + - | | | India + - | | | 3 + - | | | - 5 | Japan | 3 | + - | | | JP + - | | | Japan + - | | | 3Sinzo Abe+ - | | | -(2 rows) - -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH './*'); - id | COUNTRY_NAME | REGION_ID | rawdata -----+--------------+-----------+----------------------------------------------------------------------------------------------------------------------------- - 4 | India | 3 | INIndia3 - 5 | Japan | 3 | JPJapan3Sinzo Abe -(2 rows) - -SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text); - element ----------------------- - a1aa2a bbbbxxxcccc -(1 row) - -SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text PATH 'element/text()'); -- should fail -ERROR: more than one value returned by column XPath expression --- CDATA test -select * from xmltable('d/r' passing ' &"<>!foo]]>2' columns c text); - c -------------------------- - &"<>!foo - 2 -(2 rows) - --- XML builtin entities -SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent text); - ent ------ - ' - " - & - < - > -(5 rows) - -SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent xml); - ent ------------------- - ' - " - & - < - > -(5 rows) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) -(7 rows) - --- test qual -SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan'; - COUNTRY_NAME | REGION_ID ---------------+----------- - Japan | 3 -(1 row) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT f.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) AS f WHERE "COUNTRY_NAME" = 'Japan'; - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Nested Loop - Output: f."COUNTRY_NAME", f."REGION_ID" - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" f - Output: f."COUNTRY_NAME", f."REGION_ID" - Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer) - Filter: (f."COUNTRY_NAME" = 'Japan'::text) -(8 rows) - -EXPLAIN (VERBOSE, FORMAT JSON, COSTS OFF) -SELECT f.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) AS f WHERE "COUNTRY_NAME" = 'Japan'; - QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - [ + - { + - "Plan": { + - "Node Type": "Nested Loop", + - "Parallel Aware": false, + - "Async Capable": false, + - "Join Type": "Inner", + - "Disabled": false, + - "Output": ["f.\"COUNTRY_NAME\"", "f.\"REGION_ID\""], + - "Inner Unique": false, + - "Plans": [ + - { + - "Node Type": "Seq Scan", + - "Parent Relationship": "Outer", + - "Parallel Aware": false, + - "Async Capable": false, + - "Relation Name": "xmldata", + - "Schema": "public", + - "Alias": "xmldata", + - "Disabled": false, + - "Output": ["xmldata.data"] + - }, + - { + - "Node Type": "Table Function Scan", + - "Parent Relationship": "Inner", + - "Parallel Aware": false, + - "Async Capable": false, + - "Table Function Name": "xmltable", + - "Alias": "f", + - "Disabled": false, + - "Output": ["f.\"COUNTRY_NAME\"", "f.\"REGION_ID\""], + - "Table Function Call": "XMLTABLE(('/ROWS/ROW[COUNTRY_NAME=\"Japan\" or COUNTRY_NAME=\"India\"]'::text) PASSING (xmldata.data) COLUMNS \"COUNTRY_NAME\" text, \"REGION_ID\" integer)",+ - "Filter": "(f.\"COUNTRY_NAME\" = 'Japan'::text)" + - } + - ] + - } + - } + - ] -(1 row) - --- should to work with more data -INSERT INTO xmldata VALUES(' - - CZ - Czech Republic - 2Milos Zeman - - - DE - Germany - 2 - - - FR - France - 2 - -'); -INSERT INTO xmldata VALUES(' - - EG - Egypt - 1 - - - SD - Sudan - 1 - -'); -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+----------------+------------+-----------+------+------+--------------- - 1 | 1 | Australia | AU | 3 | | | not specified - 2 | 2 | China | CN | 3 | | | not specified - 3 | 3 | HongKong | HK | 3 | | | not specified - 4 | 4 | India | IN | 3 | | | not specified - 5 | 5 | Japan | JP | 3 | | | Sinzo Abe - 6 | 6 | Singapore | SG | 3 | 791 | km | not specified - 10 | 1 | Czech Republic | CZ | 2 | | | Milos Zeman - 11 | 2 | Germany | DE | 2 | | | not specified - 12 | 3 | France | FR | 2 | | | not specified - 20 | 1 | Egypt | EG | 1 | | | not specified - 21 | 2 | Sudan | SD | 1 | | | not specified -(11 rows) - -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') - WHERE region_id = 2; - id | _id | country_name | country_id | region_id | size | unit | premier_name -----+-----+----------------+------------+-----------+------+------+--------------- - 10 | 1 | Czech Republic | CZ | 2 | | | Milos Zeman - 11 | 2 | Germany | DE | 2 | | | not specified - 12 | 3 | France | FR | 2 | | | not specified -(3 rows) - -EXPLAIN (VERBOSE, COSTS OFF) -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE', - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') - WHERE region_id = 2; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - Nested Loop - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - -> Seq Scan on public.xmldata - Output: xmldata.data - -> Table Function Scan on "xmltable" - Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name - Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) - Filter: ("xmltable".region_id = 2) -(8 rows) - --- should fail, NULL value -SELECT xmltable.* - FROM (SELECT data FROM xmldata) x, - LATERAL XMLTABLE('/ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - _id FOR ORDINALITY, - country_name text PATH 'COUNTRY_NAME' NOT NULL, - country_id text PATH 'COUNTRY_ID', - region_id int PATH 'REGION_ID', - size float PATH 'SIZE' NOT NULL, - unit text PATH 'SIZE/@unit', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); -ERROR: null is not allowed in column "size" --- if all is ok, then result is empty --- one line xml test -WITH - x AS (SELECT proname, proowner, procost::numeric, pronargs, - array_to_string(proargnames,',') as proargnames, - case when proargtypes <> '' then array_to_string(proargtypes::oid[],',') end as proargtypes - FROM pg_proc WHERE proname = 'f_leak'), - y AS (SELECT xmlelement(name proc, - xmlforest(proname, proowner, - procost, pronargs, - proargnames, proargtypes)) as proc - FROM x), - z AS (SELECT xmltable.* - FROM y, - LATERAL xmltable('/proc' PASSING proc - COLUMNS proname name, - proowner oid, - procost float, - pronargs int, - proargnames text, - proargtypes text)) - SELECT * FROM z - EXCEPT SELECT * FROM x; - proname | proowner | procost | pronargs | proargnames | proargtypes ----------+----------+---------+----------+-------------+------------- -(0 rows) - --- multi line xml test, result should be empty too -WITH - x AS (SELECT proname, proowner, procost::numeric, pronargs, - array_to_string(proargnames,',') as proargnames, - case when proargtypes <> '' then array_to_string(proargtypes::oid[],',') end as proargtypes - FROM pg_proc), - y AS (SELECT xmlelement(name data, - xmlagg(xmlelement(name proc, - xmlforest(proname, proowner, procost, - pronargs, proargnames, proargtypes)))) as doc - FROM x), - z AS (SELECT xmltable.* - FROM y, - LATERAL xmltable('/data/proc' PASSING doc - COLUMNS proname name, - proowner oid, - procost float, - pronargs int, - proargnames text, - proargtypes text)) - SELECT * FROM z - EXCEPT SELECT * FROM x; - proname | proowner | procost | pronargs | proargnames | proargtypes ----------+----------+---------+----------+-------------+------------- -(0 rows) - -CREATE TABLE xmltest2(x xml, _path text); -INSERT INTO xmltest2 VALUES('1', 'A'); -INSERT INTO xmltest2 VALUES('2', 'B'); -INSERT INTO xmltest2 VALUES('3', 'C'); -INSERT INTO xmltest2 VALUES('2', 'D'); -SELECT xmltable.* FROM xmltest2, LATERAL xmltable('/d/r' PASSING x COLUMNS a int PATH '' || lower(_path) || 'c'); - a ---- - 1 - 2 - 3 - 2 -(4 rows) - -SELECT xmltable.* FROM xmltest2, LATERAL xmltable(('/d/r/' || lower(_path) || 'c') PASSING x COLUMNS a int PATH '.'); - a ---- - 1 - 2 - 3 - 2 -(4 rows) - -SELECT xmltable.* FROM xmltest2, LATERAL xmltable(('/d/r/' || lower(_path) || 'c') PASSING x COLUMNS a int PATH 'x' DEFAULT ascii(_path) - 54); - a ----- - 11 - 12 - 13 - 14 -(4 rows) - --- XPath result can be boolean or number too -SELECT * FROM XMLTABLE('*' PASSING 'a' COLUMNS a xml PATH '.', b text PATH '.', c text PATH '"hi"', d boolean PATH '. = "a"', e integer PATH 'string-length(.)'); - a | b | c | d | e -----------+---+----+---+--- - a | a | hi | t | 1 -(1 row) - -\x -SELECT * FROM XMLTABLE('*' PASSING 'pre&deeppost' COLUMNS x xml PATH '/e/n2', y xml PATH '/'); --[ RECORD 1 ]----------------------------------------------------------- -x | &deep -y | pre&deeppost+ - | - -\x -SELECT * FROM XMLTABLE('.' PASSING XMLELEMENT(NAME a) columns a varchar(20) PATH '""', b xml PATH '""'); - a | b ---------+-------------- - | <foo/> -(1 row) - -SELECT xmltext(NULL); - xmltext ---------- - -(1 row) - -SELECT xmltext(''); - xmltext ---------- - -(1 row) - -SELECT xmltext(' '); - xmltext ---------- - -(1 row) - -SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}'); - xmltext --------------------------- - foo `$_-+?=*^%!|/\()[]{} -(1 row) - -SELECT xmltext('foo & <"bar">'); - xmltext ------------------------------------ - foo & <"bar"> -(1 row) - -SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j'::char); - xmltext ---------------------------------- - x<P>73</P>0.42truej -(1 row) - diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql index 8eda57f4bea..d999b240d6a 100644 --- a/src/test/regress/sql/xml.sql +++ b/src/test/regress/sql/xml.sql @@ -5,7 +5,14 @@ CREATE TABLE xmltest ( INSERT INTO xmltest VALUES (1, 'one'); INSERT INTO xmltest VALUES (2, 'two'); -INSERT INTO xmltest VALUES (3, 'three '); + +-- If no XML data could be inserted, skip the tests as the server has been +-- compiled without libxml support. +SELECT count(*) = 0 AS skip_test FROM xmltest \gset +\if :skip_test +\quit +\endif SELECT * FROM xmltest; @@ -30,7 +37,7 @@ SELECT xmlconcat(xmlcomment('hello'), SELECT xmlconcat('hello', 'you'); SELECT xmlconcat(1, 2); -SELECT xmlconcat('bad', ' '); SELECT xmlconcat('', NULL, ''); SELECT xmlconcat('', NULL, ''); SELECT xmlconcat(NULL); @@ -75,17 +82,17 @@ SELECT xmlparse(content '&'); SELECT xmlparse(content '&idontexist;'); SELECT xmlparse(content ''); SELECT xmlparse(content ''); -SELECT xmlparse(content '&idontexist;'); +SELECT xmlparse(content '&idontexist; '); SELECT xmlparse(content ''); -SELECT xmlparse(document ' '); +SELECT xmlparse(document '!'); SELECT xmlparse(document 'abc'); SELECT xmlparse(document 'x'); -SELECT xmlparse(document '&'); -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '& '); +SELECT xmlparse(document '&idontexist; '); SELECT xmlparse(document ''); SELECT xmlparse(document ''); -SELECT xmlparse(document '&idontexist;'); +SELECT xmlparse(document '&idontexist; '); SELECT xmlparse(document ''); From b1ab4bc52a1fd4f8bf396baf4c1ab0a4c32f9b49 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Mon, 15 Jun 2026 11:28:45 +0300 Subject: [PATCH 067/250] Fix PQdescribePrepared with more than 7498 params If a query has more than 7498 params, the ParameterDescription message exceeds the 30000 byte limit on messages that are not specifically marked as possibly being longer than that (VALID_LONG_MESSAGE_TYPE). To fix, add ParameterDescription to the list. Author: Ning Sun Discussion: https://www.postgresql.org/message-id/dbfb4b65-0aa8-470a-8b87-b6496160b28a@gmail.com Backpatch-through: 14 --- src/interfaces/libpq/fe-protocol3.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 45b80455b0f..048851a3635 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -42,7 +42,8 @@ (id) == PqMsg_FunctionCallResponse || \ (id) == PqMsg_NoticeResponse || \ (id) == PqMsg_NotificationResponse || \ - (id) == PqMsg_RowDescription) + (id) == PqMsg_RowDescription || \ + (id) == PqMsg_ParameterDescription) static void handleFatalError(PGconn *conn); From 6603e81e69e9b7378e2581f436119a7c6cb94cf9 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 15 Jun 2026 12:22:55 -0400 Subject: [PATCH 068/250] Modernize pg_bsd_indent's error/warning reporting code. Late-model clang complains that these functions should be labeled with "format(printf, 2, 3)", and it's right. But let's go a bit further and also make use of varargs, to remove duplication and allow these functions to be used with non-integer input values. Since no good deed goes unpunished, I had to also adjust a couple of call sites. They weren't wrong as-is, since the size_t-sized arguments were coerced to int on the way into diag3(). But without that, we have to adjust the format strings. The point of this is to suppress compiler warnings, so back-patch into branches containing pg_bsd_indent, even though there's no functional change. Author: Tom Lane Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/1645041.1781283554@sss.pgh.pa.us Backpatch-through: 16 --- src/tools/pg_bsd_indent/indent.c | 4 +-- src/tools/pg_bsd_indent/indent.h | 9 ++++--- src/tools/pg_bsd_indent/io.c | 43 +++++--------------------------- 3 files changed, 14 insertions(+), 42 deletions(-) diff --git a/src/tools/pg_bsd_indent/indent.c b/src/tools/pg_bsd_indent/indent.c index 2622cc6227a..040894463fb 100644 --- a/src/tools/pg_bsd_indent/indent.c +++ b/src/tools/pg_bsd_indent/indent.c @@ -535,7 +535,7 @@ main(int argc, char **argv) case lparen: /* got a '(' or '[' */ /* count parens to make Healy happy */ if (++ps.p_l_follow == nitems(ps.paren_indents)) { - diag3(0, "Reached internal limit of %d unclosed parens", + diag3(0, "Reached internal limit of %zu unclosed parens", nitems(ps.paren_indents)); ps.p_l_follow--; } @@ -808,7 +808,7 @@ main(int argc, char **argv) * declaration or an init */ di_stack[ps.dec_nest] = dec_ind; if (++ps.dec_nest == nitems(di_stack)) { - diag3(0, "Reached internal limit of %d struct levels", + diag3(0, "Reached internal limit of %zu struct levels", nitems(di_stack)); ps.dec_nest--; } diff --git a/src/tools/pg_bsd_indent/indent.h b/src/tools/pg_bsd_indent/indent.h index e9e71d667d8..974ffe1ac27 100644 --- a/src/tools/pg_bsd_indent/indent.h +++ b/src/tools/pg_bsd_indent/indent.h @@ -39,9 +39,7 @@ int compute_label_target(void); int count_spaces(int, char *); int count_spaces_until(int, char *, char *); int lexi(struct parser_state *); -void diag2(int, const char *); -void diag3(int, const char *, int); -void diag4(int, const char *, int, int); +void diag(int level, const char *msg, ...) pg_attribute_printf(2, 3); void dump_line(void); int lookahead(void); void lookahead_reset(void); @@ -51,3 +49,8 @@ void pr_comment(void); void set_defaults(void); void set_option(char *); void set_profile(const char *); + +/* backwards-compatibility macros */ +#define diag2(level, msg) diag(level, msg) +#define diag3(level, msg, a) diag(level, msg, a) +#define diag4(level, msg, a, b) diag(level, msg, a, b) diff --git a/src/tools/pg_bsd_indent/io.c b/src/tools/pg_bsd_indent/io.c index 9d64ca1ee56..364d5744063 100644 --- a/src/tools/pg_bsd_indent/io.c +++ b/src/tools/pg_bsd_indent/io.c @@ -553,53 +553,22 @@ count_spaces(int cur, char *buffer) } void -diag4(int level, const char *msg, int a, int b) +diag(int level, const char *msg, ...) { - if (level) - found_err = 1; - if (output == stdout) { - fprintf(stdout, "/**INDENT** %s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stdout, msg, a, b); - fprintf(stdout, " */\n"); - } - else { - fprintf(stderr, "%s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stderr, msg, a, b); - fprintf(stderr, "\n"); - } -} - -void -diag3(int level, const char *msg, int a) -{ - if (level) - found_err = 1; - if (output == stdout) { - fprintf(stdout, "/**INDENT** %s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stdout, msg, a); - fprintf(stdout, " */\n"); - } - else { - fprintf(stderr, "%s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stderr, msg, a); - fprintf(stderr, "\n"); - } -} + va_list ap; -void -diag2(int level, const char *msg) -{ + va_start(ap, msg); if (level) found_err = 1; if (output == stdout) { fprintf(stdout, "/**INDENT** %s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stdout, "%s", msg); + vfprintf(stdout, msg, ap); fprintf(stdout, " */\n"); } else { fprintf(stderr, "%s@%d: ", level == 0 ? "Warning" : "Error", line_no); - fprintf(stderr, "%s", msg); + vfprintf(stderr, msg, ap); fprintf(stderr, "\n"); } + va_end(ap); } - From 85e6624c06a187cb53ab8edc4dab1c47de6f898d Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 15 Jun 2026 12:16:38 -0500 Subject: [PATCH 069/250] doc: Fix "Prev" link. Presently, the "Prev" link on the page for background workers sends you to the middle of the previous chapter instead of the actual previous page. This appears to be caused by a libxml2 bug, but regardless, a minimal fix is to change the link generation code to use [position()=last()] instead of [last()] in the predicate on the union of reverse axes. Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/aim4AZorFKaC7Wrf%40nathan Backpatch-through: 14 --- doc/src/sgml/stylesheet-speedup-xhtml.xsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/stylesheet-speedup-xhtml.xsl b/doc/src/sgml/stylesheet-speedup-xhtml.xsl index da0f2b5a970..a3b3692ba03 100644 --- a/doc/src/sgml/stylesheet-speedup-xhtml.xsl +++ b/doc/src/sgml/stylesheet-speedup-xhtml.xsl @@ -208,7 +208,7 @@ |ancestor::article[1] |ancestor::topic[1] |preceding::sect1[1] - |ancestor::sect1[1])[last()]"/> + |ancestor::sect1[1])[position()=last()]"/> slotname); This is fine as long as options->slotname doesn't contain a double quote mark, but what if it does? In principle this'd allow injection of harmful options into replication commands, in the probably-unlikely case that a slot name comes from untrustworthy input. We ought to clean that up. Moreover, even the places that were trying to be more careful generally got it wrong, because they used quoting subroutines intended for SQL commands rather than something that will work with the replication-command scanner repl_scanner.l. For example, several places naively use PQescapeLiteral() to quote option values for replication commands. If the string contains a backslash, PQescapeLiteral() will produce E'...' literal syntax, which repl_scanner.l doesn't recognize. Another near miss was to use quote_identifier() to quote identifiers. That function won't quote valid lowercase identifiers unless they match SQL keywords ... but in this context, replication keywords are what matter. Neither of these errors seem to risk string injection, but they definitely can cause syntax errors in replication commands that ought to be valid. We can clean all this up by using simple quoting logic that just doubles single or double quotes respectively. Or at least, we could if repl_scanner.l handled doubled double quotes in identifiers, but for some reason it doesn't! So the first step in this fix has to be to fix that. (The fact that we'll later reject slot names containing double quotes is very far short of justifying this omission.) Having done that, this patch runs around and applies correct quoting in all places that generate replication commands containing strings coming from outside the immediate context. Probably some of these places are safe because of restrictions elsewhere, but it seems best to just quote all the time. This was originally reported as a security bug, which it could be if replication slot names or parameters were to originate from untrustworthy sources. But the security team concluded that that was a very improbable situation, so we're just going to fix this as a regular bug. Reported-by: Team Dhiutsa Author: Tom Lane Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/1648659.1781287310@sss.pgh.pa.us Backpatch-through: 14 --- src/backend/commands/subscriptioncmds.c | 30 ++++++- .../libpqwalreceiver/libpqwalreceiver.c | 88 +++++++++++-------- src/backend/replication/repl_scanner.l | 4 + src/bin/pg_basebackup/pg_recvlogical.c | 14 +-- src/bin/pg_basebackup/receivelog.c | 28 +++--- src/bin/pg_basebackup/streamutil.c | 57 ++++++++---- src/bin/pg_basebackup/streamutil.h | 11 ++- 7 files changed, 159 insertions(+), 73 deletions(-) diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index ee0a0169cc9..395dcc678c2 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -439,6 +439,32 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, } } +/* + * Append a suitably-quoted identifier or string literal to buf. + * "quote" should be either a double-quote or single-quote character. + * + * Caution: this quoting logic is sufficient for identifiers and literals + * in the replication grammar, but not always in regular SQL. Specifically, + * it'd fail for a string literal if standard_conforming_strings is off. + */ +static void +appendQuotedString(StringInfo buf, const char *str, char quote) +{ + appendStringInfoChar(buf, quote); + while (*str) + { + char c = *str++; + + if (c == quote) + appendStringInfoChar(buf, c); + appendStringInfoChar(buf, c); + } + appendStringInfoChar(buf, quote); +} + +#define appendQuotedIdentifier(b, s) appendQuotedString(b, s, '"') +#define appendQuotedLiteral(b, s) appendQuotedString(b, s, '\'') + /* * Check that the specified publications are present on the publisher. */ @@ -1921,7 +1947,9 @@ ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missi load_file("libpqwalreceiver", false); initStringInfo(&cmd); - appendStringInfo(&cmd, "DROP_REPLICATION_SLOT %s WAIT", quote_identifier(slotname)); + appendStringInfoString(&cmd, "DROP_REPLICATION_SLOT "); + appendQuotedIdentifier(&cmd, slotname); + appendStringInfoString(&cmd, " WAIT"); PG_TRY(); { diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c index a3ceb666f5e..7fc4ebd76bf 100644 --- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c +++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c @@ -116,7 +116,7 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = { }; /* Prototypes for private functions */ -static char *stringlist_to_identifierstr(PGconn *conn, List *strings); +static char *stringlist_to_identifierstr(List *strings); /* * Module initialization function @@ -528,6 +528,32 @@ libpqrcv_get_option_from_conninfo(const char *connInfo, const char *keyword) return option; } +/* + * Append a suitably-quoted identifier or string literal to buf. + * "quote" should be either a double-quote or single-quote character. + * + * Caution: this quoting logic is sufficient for identifiers and literals + * in the replication grammar, but not always in regular SQL. Specifically, + * it'd fail for a string literal if standard_conforming_strings is off. + */ +static void +appendQuotedString(StringInfo buf, const char *str, char quote) +{ + appendStringInfoChar(buf, quote); + while (*str) + { + char c = *str++; + + if (c == quote) + appendStringInfoChar(buf, c); + appendStringInfoChar(buf, c); + } + appendStringInfoChar(buf, quote); +} + +#define appendQuotedIdentifier(b, s) appendQuotedString(b, s, '"') +#define appendQuotedLiteral(b, s) appendQuotedString(b, s, '\'') + /* * Start streaming WAL data from given streaming options. * @@ -553,8 +579,10 @@ libpqrcv_startstreaming(WalReceiverConn *conn, /* Build the command. */ appendStringInfoString(&cmd, "START_REPLICATION"); if (options->slotname != NULL) - appendStringInfo(&cmd, " SLOT \"%s\"", - options->slotname); + { + appendStringInfoString(&cmd, " SLOT "); + appendQuotedIdentifier(&cmd, options->slotname); + } if (options->logical) appendStringInfoString(&cmd, " LOGICAL"); @@ -569,7 +597,6 @@ libpqrcv_startstreaming(WalReceiverConn *conn, { char *pubnames_str; List *pubnames; - char *pubnames_literal; appendStringInfoString(&cmd, " ("); @@ -577,8 +604,10 @@ libpqrcv_startstreaming(WalReceiverConn *conn, options->proto.logical.proto_version); if (options->proto.logical.streaming_str) - appendStringInfo(&cmd, ", streaming '%s'", - options->proto.logical.streaming_str); + { + appendStringInfoString(&cmd, ", streaming "); + appendQuotedLiteral(&cmd, options->proto.logical.streaming_str); + } if (options->proto.logical.twophase && PQserverVersion(conn->streamConn) >= 150000) @@ -586,25 +615,15 @@ libpqrcv_startstreaming(WalReceiverConn *conn, if (options->proto.logical.origin && PQserverVersion(conn->streamConn) >= 160000) - appendStringInfo(&cmd, ", origin '%s'", - options->proto.logical.origin); + { + appendStringInfoString(&cmd, ", origin "); + appendQuotedLiteral(&cmd, options->proto.logical.origin); + } pubnames = options->proto.logical.publication_names; - pubnames_str = stringlist_to_identifierstr(conn->streamConn, pubnames); - if (!pubnames_str) - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), /* likely guess */ - errmsg("could not start WAL streaming: %s", - pchomp(PQerrorMessage(conn->streamConn))))); - pubnames_literal = PQescapeLiteral(conn->streamConn, pubnames_str, - strlen(pubnames_str)); - if (!pubnames_literal) - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), /* likely guess */ - errmsg("could not start WAL streaming: %s", - pchomp(PQerrorMessage(conn->streamConn))))); - appendStringInfo(&cmd, ", publication_names %s", pubnames_literal); - PQfreemem(pubnames_literal); + pubnames_str = stringlist_to_identifierstr(pubnames); + appendStringInfoString(&cmd, ", publication_names "); + appendQuotedLiteral(&cmd, pubnames_str); pfree(pubnames_str); if (options->proto.logical.binary && @@ -920,7 +939,8 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname, initStringInfo(&cmd); - appendStringInfo(&cmd, "CREATE_REPLICATION_SLOT \"%s\"", slotname); + appendStringInfoString(&cmd, "CREATE_REPLICATION_SLOT "); + appendQuotedIdentifier(&cmd, slotname); if (temporary) appendStringInfoString(&cmd, " TEMPORARY"); @@ -1029,8 +1049,9 @@ libpqrcv_alter_slot(WalReceiverConn *conn, const char *slotname, PGresult *res; initStringInfo(&cmd); - appendStringInfo(&cmd, "ALTER_REPLICATION_SLOT %s ( ", - quote_identifier(slotname)); + appendStringInfoString(&cmd, "ALTER_REPLICATION_SLOT "); + appendQuotedIdentifier(&cmd, slotname); + appendStringInfoString(&cmd, " ( "); if (failover) appendStringInfo(&cmd, "FAILOVER %s", @@ -1226,10 +1247,10 @@ libpqrcv_exec(WalReceiverConn *conn, const char *query, * * This is essentially the reverse of SplitIdentifierString. * - * The caller should free the result. + * The caller should pfree the result. */ static char * -stringlist_to_identifierstr(PGconn *conn, List *strings) +stringlist_to_identifierstr(List *strings) { ListCell *lc; StringInfoData res; @@ -1240,21 +1261,12 @@ stringlist_to_identifierstr(PGconn *conn, List *strings) foreach(lc, strings) { char *val = strVal(lfirst(lc)); - char *val_escaped; if (first) first = false; else appendStringInfoChar(&res, ','); - - val_escaped = PQescapeIdentifier(conn, val, strlen(val)); - if (!val_escaped) - { - free(res.data); - return NULL; - } - appendStringInfoString(&res, val_escaped); - PQfreemem(val_escaped); + appendQuotedIdentifier(&res, val); } return res.data; diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l index 014ea8d25c6..03a34a39f4d 100644 --- a/src/backend/replication/repl_scanner.l +++ b/src/backend/replication/repl_scanner.l @@ -197,6 +197,10 @@ UPLOAD_MANIFEST { return K_UPLOAD_MANIFEST; } return IDENT; } +{xddouble} { + addlitchar('"', yyscanner); + } + {xdinside} { addlit(yytext, yyleng, yyscanner); } diff --git a/src/bin/pg_basebackup/pg_recvlogical.c b/src/bin/pg_basebackup/pg_recvlogical.c index 1e8b149d4e7..113a43b81ad 100644 --- a/src/bin/pg_basebackup/pg_recvlogical.c +++ b/src/bin/pg_basebackup/pg_recvlogical.c @@ -244,8 +244,9 @@ StreamLogicalLog(void) /* Initiate the replication stream at specified location */ query = createPQExpBuffer(); - appendPQExpBuffer(query, "START_REPLICATION SLOT \"%s\" LOGICAL %X/%X", - replication_slot, LSN_FORMAT_ARGS(startpos)); + appendPQExpBufferStr(query, "START_REPLICATION SLOT "); + AppendQuotedIdentifier(query, replication_slot); + appendPQExpBuffer(query, " LOGICAL %X/%X", LSN_FORMAT_ARGS(startpos)); /* print options if there are any */ if (noptions) @@ -258,11 +259,14 @@ StreamLogicalLog(void) appendPQExpBufferStr(query, ", "); /* write option name */ - appendPQExpBuffer(query, "\"%s\"", options[(i * 2)]); + AppendQuotedIdentifier(query, options[i * 2]); /* write option value if specified */ - if (options[(i * 2) + 1] != NULL) - appendPQExpBuffer(query, " '%s'", options[(i * 2) + 1]); + if (options[i * 2 + 1] != NULL) + { + appendPQExpBufferChar(query, ' '); + AppendQuotedLiteral(query, options[i * 2 + 1]); + } } if (noptions) diff --git a/src/bin/pg_basebackup/receivelog.c b/src/bin/pg_basebackup/receivelog.c index 6b6e32dfbdf..c9c7bbadc9a 100644 --- a/src/bin/pg_basebackup/receivelog.c +++ b/src/bin/pg_basebackup/receivelog.c @@ -451,8 +451,7 @@ CheckServerVersionForStreaming(PGconn *conn) bool ReceiveXlogStream(PGconn *conn, StreamCtl *stream) { - char query[128]; - char slotcmd[128]; + PQExpBuffer query; PGresult *res; XLogRecPtr stoppos; @@ -477,7 +476,6 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) if (stream->replication_slot != NULL) { reportFlushPosition = true; - sprintf(slotcmd, "SLOT \"%s\" ", stream->replication_slot); } else { @@ -485,7 +483,6 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) reportFlushPosition = true; else reportFlushPosition = false; - slotcmd[0] = 0; } if (stream->sysidentifier != NULL) @@ -534,8 +531,10 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) */ if (!existsTimeLineHistoryFile(stream)) { - snprintf(query, sizeof(query), "TIMELINE_HISTORY %u", stream->timeline); - res = PQexec(conn, query); + query = createPQExpBuffer(); + appendPQExpBuffer(query, "TIMELINE_HISTORY %u", stream->timeline); + res = PQexec(conn, query->data); + destroyPQExpBuffer(query); if (PQresultStatus(res) != PGRES_TUPLES_OK) { /* FIXME: we might send it ok, but get an error */ @@ -571,11 +570,18 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) return true; /* Initiate the replication stream at specified location */ - snprintf(query, sizeof(query), "START_REPLICATION %s%X/%X TIMELINE %u", - slotcmd, - LSN_FORMAT_ARGS(stream->startpos), - stream->timeline); - res = PQexec(conn, query); + query = createPQExpBuffer(); + appendPQExpBufferStr(query, "START_REPLICATION"); + if (stream->replication_slot != NULL) + { + appendPQExpBufferStr(query, " SLOT "); + AppendQuotedIdentifier(query, stream->replication_slot); + } + appendPQExpBuffer(query, " %X/%X TIMELINE %u", + LSN_FORMAT_ARGS(stream->startpos), + stream->timeline); + res = PQexec(conn, query->data); + destroyPQExpBuffer(query); if (PQresultStatus(res) != PGRES_COPY_BOTH) { pg_log_error("could not send replication command \"%s\": %s", diff --git a/src/bin/pg_basebackup/streamutil.c b/src/bin/pg_basebackup/streamutil.c index c7b8a4c3a4b..a7b4fc084ae 100644 --- a/src/bin/pg_basebackup/streamutil.c +++ b/src/bin/pg_basebackup/streamutil.c @@ -501,7 +501,8 @@ GetSlotInformation(PGconn *conn, const char *slot_name, *restart_tli = tli_loc; query = createPQExpBuffer(); - appendPQExpBuffer(query, "READ_REPLICATION_SLOT %s", slot_name); + appendPQExpBufferStr(query, "READ_REPLICATION_SLOT "); + AppendQuotedIdentifier(query, slot_name); res = PQexec(conn, query->data); destroyPQExpBuffer(query); @@ -598,13 +599,17 @@ CreateReplicationSlot(PGconn *conn, const char *slot_name, const char *plugin, Assert(slot_name != NULL); /* Build base portion of query */ - appendPQExpBuffer(query, "CREATE_REPLICATION_SLOT \"%s\"", slot_name); + appendPQExpBufferStr(query, "CREATE_REPLICATION_SLOT "); + AppendQuotedIdentifier(query, slot_name); if (is_temporary) appendPQExpBufferStr(query, " TEMPORARY"); if (is_physical) appendPQExpBufferStr(query, " PHYSICAL"); else - appendPQExpBuffer(query, " LOGICAL \"%s\"", plugin); + { + appendPQExpBufferStr(query, " LOGICAL "); + AppendQuotedIdentifier(query, plugin); + } /* Add any requested options */ if (use_new_option_syntax) @@ -704,8 +709,8 @@ DropReplicationSlot(PGconn *conn, const char *slot_name) query = createPQExpBuffer(); /* Build query */ - appendPQExpBuffer(query, "DROP_REPLICATION_SLOT \"%s\"", - slot_name); + appendPQExpBufferStr(query, "DROP_REPLICATION_SLOT "); + AppendQuotedIdentifier(query, slot_name); res = PQexec(conn, query->data); if (PQresultStatus(res) != PGRES_COMMAND_OK) { @@ -733,6 +738,29 @@ DropReplicationSlot(PGconn *conn, const char *slot_name) return true; } +/* + * Append a suitably-quoted identifier or string literal to buf. + * "quote" should be either a double-quote or single-quote character. + * + * Caution: this quoting logic is sufficient for identifiers and literals + * in the replication grammar, but not always in regular SQL. Specifically, + * it'd fail for a string literal if standard_conforming_strings is off. + */ +void +AppendQuotedString(PQExpBuffer buf, const char *str, char quote) +{ + appendPQExpBufferChar(buf, quote); + while (*str) + { + char c = *str++; + + if (c == quote) + appendPQExpBufferChar(buf, c); + appendPQExpBufferChar(buf, c); + } + appendPQExpBufferChar(buf, quote); +} + /* * Append a "plain" option - one with no value - to a server command that * is being constructed. @@ -741,10 +769,13 @@ DropReplicationSlot(PGconn *conn, const char *slot_name) * write things like SOME_COMMAND OPTION1 OPTION2 'opt2value' OPTION3 42. The * new syntax uses a comma-separated list surrounded by parentheses, so the * equivalent is SOME_COMMAND (OPTION1, OPTION2 'optvalue', OPTION3 42). + * + * Note: we assume option names do not require quotes. Do not use this + * with option names coming from outside sources. */ void AppendPlainCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name) + const char *option_name) { if (buf->len > 0 && buf->data[buf->len - 1] != '(') { @@ -765,30 +796,26 @@ AppendPlainCommandOption(PQExpBuffer buf, bool use_new_option_syntax, */ void AppendStringCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name, char *option_value) + const char *option_name, const char *option_value) { AppendPlainCommandOption(buf, use_new_option_syntax, option_name); if (option_value != NULL) { - size_t length = strlen(option_value); - char *escaped_value = palloc(1 + 2 * length); - - PQescapeStringConn(conn, escaped_value, option_value, length, NULL); - appendPQExpBuffer(buf, " '%s'", escaped_value); - pfree(escaped_value); + appendPQExpBufferChar(buf, ' '); + AppendQuotedLiteral(buf, option_value); } } /* - * Append an option with an associated integer value to a server command + * Append an option with an associated integer value to a server command that * is being constructed. * * See comments for AppendPlainCommandOption, above. */ void AppendIntegerCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name, int32 option_value) + const char *option_name, int32 option_value) { AppendPlainCommandOption(buf, use_new_option_syntax, option_name); diff --git a/src/bin/pg_basebackup/streamutil.h b/src/bin/pg_basebackup/streamutil.h index 017b227303c..ee2e9835080 100644 --- a/src/bin/pg_basebackup/streamutil.h +++ b/src/bin/pg_basebackup/streamutil.h @@ -43,15 +43,20 @@ extern bool RunIdentifySystem(PGconn *conn, char **sysid, XLogRecPtr *startpos, char **db_name); +extern void AppendQuotedString(PQExpBuffer buf, const char *str, char quote); +#define AppendQuotedIdentifier(b, s) AppendQuotedString(b, s, '"') +#define AppendQuotedLiteral(b, s) AppendQuotedString(b, s, '\'') extern void AppendPlainCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name); + const char *option_name); extern void AppendStringCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name, char *option_value); + const char *option_name, + const char *option_value); extern void AppendIntegerCommandOption(PQExpBuffer buf, bool use_new_option_syntax, - char *option_name, int32 option_value); + const char *option_name, + int32 option_value); extern bool GetSlotInformation(PGconn *conn, const char *slot_name, XLogRecPtr *restart_lsn, From 42ffdedcf743c23ba346eba8e3afce9da5e8f618 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 16 Jun 2026 08:22:41 +0900 Subject: [PATCH 071/250] Fix inconsistencies with pg_restore --statistics[-only] Attempting to restore a schema, a table or an index with --only-statistics skipped all the statistics of the objects wanted. Like for pg_dump, statistics should be included, so this created an assymetry between dump and restore. A second set of problems existed for --table and --index, where the presence of --statistics skipped the restore of the stats of the object(s) targetted. This issue has been reported originally as related to an inconsistency with the way extended stats restore is handled in Postgres v19, but the issue is related to the restore of relation and attribute statistics in v18. Some TAP tests are added to cover all these cases. Reported-by: Chao Li Author: Chao Li Author: Michael Paquier Reviewed-by: Corey Huinker Discussion: https://postgr.es/m/66E80CAB-527C-42B1-BB65-3F82CF4AD998@gmail.com Backpatch-through: 18 --- src/bin/pg_dump/pg_backup_archiver.c | 36 ++++++++++++----- src/bin/pg_dump/t/002_pg_dump.pl | 59 ++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 4293e20b20e..c4e17359d9f 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3115,7 +3115,6 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) */ if (strcmp(te->desc, "ACL") == 0 || strcmp(te->desc, "COMMENT") == 0 || - strcmp(te->desc, "STATISTICS DATA") == 0 || strcmp(te->desc, "SECURITY LABEL") == 0) { /* Database properties react to createDB, not selectivity options. */ @@ -3186,14 +3185,33 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) if (ropt->selTypes) { - if (strcmp(te->desc, "TABLE") == 0 || - strcmp(te->desc, "TABLE DATA") == 0 || - strcmp(te->desc, "VIEW") == 0 || - strcmp(te->desc, "FOREIGN TABLE") == 0 || - strcmp(te->desc, "MATERIALIZED VIEW") == 0 || - strcmp(te->desc, "MATERIALIZED VIEW DATA") == 0 || - strcmp(te->desc, "SEQUENCE") == 0 || - strcmp(te->desc, "SEQUENCE SET") == 0) + if (strcmp(te->desc, "STATISTICS DATA") == 0) + { + bool dumpthis = false; + + /* + * Statistics data can be assigned for tables or indexes, so + * check both. + */ + if (ropt->selTable && + (ropt->tableNames.head == NULL || + simple_string_list_member(&ropt->tableNames, te->tag))) + dumpthis = true; + if (ropt->selIndex && + (ropt->indexNames.head == NULL || + simple_string_list_member(&ropt->indexNames, te->tag))) + dumpthis = true; + if (!dumpthis) + return 0; + } + else if (strcmp(te->desc, "TABLE") == 0 || + strcmp(te->desc, "TABLE DATA") == 0 || + strcmp(te->desc, "VIEW") == 0 || + strcmp(te->desc, "FOREIGN TABLE") == 0 || + strcmp(te->desc, "MATERIALIZED VIEW") == 0 || + strcmp(te->desc, "MATERIALIZED VIEW DATA") == 0 || + strcmp(te->desc, "SEQUENCE") == 0 || + strcmp(te->desc, "SEQUENCE SET") == 0) { if (!ropt->selTable) return 0; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 9512e2d89e5..dae5641f24e 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -851,6 +851,60 @@ 'postgres', ], }, + statistics_only_with_schema => { + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_schema.dump", + '--statistics-only', + '--schema' => 'dump_test', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_schema.sql", + '--statistics-only', + '--schema' => 'dump_test', + "$tempdir/statistics_only_with_schema.dump", + ], + }, + statistics_only_with_table => { + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_table.dump", + '--statistics', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_table.sql", + '--statistics-only', + '--table' => 'test_table', + '--schema' => 'dump_test', + "$tempdir/statistics_only_with_table.dump", + ], + }, + statistics_only_with_index => { + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_index.dump", + '--statistics', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--format' => 'custom', + '--file' => "$tempdir/statistics_only_with_index.sql", + '--statistics-only', + '--index' => '"dump_test"\'s post-data index', + '--schema' => 'dump_test', + "$tempdir/statistics_only_with_index.dump", + ], + }, no_schema => { dump_cmd => [ 'pg_dump', '--no-sync', @@ -5137,6 +5191,8 @@ no_schema => 1, section_post_data => 1, statistics_only => 1, + statistics_only_with_schema => 1, + statistics_only_with_index => 1, schema_only_with_statistics => 1, }, unlike => { @@ -5166,6 +5222,9 @@ section_data => 1, section_post_data => 1, statistics_only => 1, + statistics_only_with_schema => 1, + statistics_only_with_index => 1, + statistics_only_with_table => 1, schema_only_with_statistics => 1, }, unlike => { From 54ffa74c9924a14691bb4cb196cb49e43a9fbd76 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 16 Jun 2026 08:31:43 +0900 Subject: [PATCH 072/250] pg_dump: Remove dead code in TAP tests The schema_only_with_statistics test scenario was referenced in 002_pg_dump.pl, but was associated to no command sequence since 0ed92cf50cc4. Issue discovered while investigating a different bug. Perhaps this cleanup is not worth backpatching, but there is also an argument in favor of reducing noise when touching this area of the code in stable branches. Reviewed-by: Ewan Young Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/ai-y0S7Z25NlrG_n@paquier.xyz Backpatch-through: 18 --- src/bin/pg_dump/t/002_pg_dump.pl | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index dae5641f24e..99bdbcd0867 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -985,8 +985,7 @@ no_table_access_method => 1, pg_dumpall_dbprivs => 1, pg_dumpall_exclude => 1, - schema_only => 1, - schema_only_with_statistics => 1,); + schema_only => 1,); # This is where the actual tests are defined. my %tests = ( @@ -1202,7 +1201,6 @@ no_large_objects => 1, no_owner => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -1756,7 +1754,6 @@ }, unlike => { schema_only => 1, - schema_only_with_statistics => 1, no_large_objects => 1, }, }, @@ -1781,7 +1778,6 @@ binary_upgrade => 1, no_large_objects => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -1804,7 +1800,6 @@ binary_upgrade => 1, no_large_objects => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -1971,7 +1966,6 @@ unlike => { no_large_objects => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -2154,7 +2148,6 @@ exclude_test_table => 1, exclude_test_table_data => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -2180,7 +2173,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -2221,7 +2213,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -2245,7 +2236,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -2270,7 +2260,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -2294,7 +2283,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -2318,7 +2306,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -3800,7 +3787,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -3973,7 +3959,6 @@ unlike => { binary_upgrade => 1, schema_only => 1, - schema_only_with_statistics => 1, exclude_measurement => 1, only_dump_test_schema => 1, test_schema_plus_large_objects => 1, @@ -4856,7 +4841,6 @@ no_large_objects => 1, no_privs => 1, schema_only => 1, - schema_only_with_statistics => 1, }, }, @@ -4975,7 +4959,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -4992,7 +4975,6 @@ binary_upgrade => 1, exclude_dump_test_schema => 1, schema_only => 1, - schema_only_with_statistics => 1, only_dump_measurement => 1, }, }, @@ -5193,7 +5175,6 @@ statistics_only => 1, statistics_only_with_schema => 1, statistics_only_with_index => 1, - schema_only_with_statistics => 1, }, unlike => { exclude_dump_test_schema => 1, @@ -5225,7 +5206,6 @@ statistics_only_with_schema => 1, statistics_only_with_index => 1, statistics_only_with_table => 1, - schema_only_with_statistics => 1, }, unlike => { no_statistics => 1, From c3e36a9a5f19bb7c2df07bc70d799a8bca682d8b Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Tue, 16 Jun 2026 09:27:00 +0300 Subject: [PATCH 073/250] Fix int32 overflow in ltree_compare() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The expression (len_diff * 10 * (an + 1)) used as the return value of ltree_compare() is computed at int32 width. With LTREE_MAX_LEVELS = 65535, the product can exceed INT32_MAX once an ltree has more than ~14,653 levels, which causes the result to wrap and invert its sign. That corrupts btree ordering as well as the "magnitude" consumed by ltree_penalty() for GiST page splits. To fix, split ltree_compare() into two functions. The new ltree_compare_distance() function returns a float, which won't overflow. It's used by the ltree_penalty() caller. All the other callers only care about the sign of the return value, i.e. which of the arguments is greater, so change ltree_compare() to not multiply the result with (10 * (an + 1)), which avoids the overflow for those callers. Existing btree or GiST indexes on ltree columns containing values with more than ~14,653 levels may be corrupt and should be REINDEXed. Add a regression test based on the reporter's PoC. Author: Ayush Tiwari Reported-by: 王跃林 Discussion: https://www.postgresql.org/message-id/AI6AnABgKW93Qbx1jVzi84r9.8.1781322625756.Hmail.3020001251%40tju.edu.cn Backpatch-through: 14 --- contrib/ltree/expected/ltree.out | 10 +++++++ contrib/ltree/ltree.h | 1 + contrib/ltree/ltree_gist.c | 6 ++-- contrib/ltree/ltree_op.c | 49 ++++++++++++++++++++++++++++---- contrib/ltree/sql/ltree.sql | 6 ++++ 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out index 108df668bf7..d4a89f28ce1 100644 --- a/contrib/ltree/expected/ltree.out +++ b/contrib/ltree/expected/ltree.out @@ -8224,3 +8224,13 @@ DETAIL: Total size of level exceeds the maximum allowed (65535 bytes). SELECT (repeat('a|', 65535) || 'a')::lquery; ERROR: lquery level has too many variants DETAIL: Number of variants exceeds the maximum allowed (65535). +-- Test that ltree_compare() does not overflow with very deep paths. +WITH s AS (SELECT 'a'::ltree AS v), + l AS (SELECT (repeat('a.', 14999) || 'a')::ltree AS v) +SELECT (l.v > s.v) AS gt_ok, (l.v < s.v) AS lt_ok, (l.v = s.v) AS eq_ok + FROM s, l; + gt_ok | lt_ok | eq_ok +-------+-------+------- + t | f | f +(1 row) + diff --git a/contrib/ltree/ltree.h b/contrib/ltree/ltree.h index 226c1cb2115..89c5b932292 100644 --- a/contrib/ltree/ltree.h +++ b/contrib/ltree/ltree.h @@ -206,6 +206,7 @@ bool ltree_execute(ITEM *curitem, void *checkval, bool calcnot, bool (*chkcond) (void *checkval, ITEM *val)); int ltree_compare(const ltree *a, const ltree *b); +float ltree_compare_distance(const ltree *a, const ltree *b); bool inner_isparent(const ltree *c, const ltree *p); bool compare_subnode(ltree_level *t, char *qn, int len, bool prefix, bool ci); ltree *lca_inner(ltree **a, int len); diff --git a/contrib/ltree/ltree_gist.c b/contrib/ltree/ltree_gist.c index 932f69bff2d..3e48aa382e2 100644 --- a/contrib/ltree/ltree_gist.c +++ b/contrib/ltree/ltree_gist.c @@ -264,11 +264,11 @@ ltree_penalty(PG_FUNCTION_ARGS) ltree_gist *newval = (ltree_gist *) DatumGetPointer(((GISTENTRY *) PG_GETARG_POINTER(1))->key); float *penalty = (float *) PG_GETARG_POINTER(2); int siglen = LTREE_GET_SIGLEN(); - int32 cmpr, + float cmpr, cmpl; - cmpl = ltree_compare(LTG_GETLNODE(origval, siglen), LTG_GETLNODE(newval, siglen)); - cmpr = ltree_compare(LTG_GETRNODE(newval, siglen), LTG_GETRNODE(origval, siglen)); + cmpl = ltree_compare_distance(LTG_GETLNODE(origval, siglen), LTG_GETLNODE(newval, siglen)); + cmpr = ltree_compare_distance(LTG_GETRNODE(newval, siglen), LTG_GETRNODE(origval, siglen)); *penalty = Max(cmpl, 0) + Max(cmpr, 0); diff --git a/contrib/ltree/ltree_op.c b/contrib/ltree/ltree_op.c index ce9f4caad4f..18368c4bc7d 100644 --- a/contrib/ltree/ltree_op.c +++ b/contrib/ltree/ltree_op.c @@ -42,6 +42,9 @@ PG_FUNCTION_INFO_V1(ltree2text); PG_FUNCTION_INFO_V1(text2ltree); PG_FUNCTION_INFO_V1(ltreeparentsel); +/* + * btree-comparison function. + */ int ltree_compare(const ltree *a, const ltree *b) { @@ -54,18 +57,52 @@ ltree_compare(const ltree *a, const ltree *b) { int res; - if ((res = memcmp(al->name, bl->name, Min(al->len, bl->len))) == 0) + res = memcmp(al->name, bl->name, Min(al->len, bl->len)); + if (res == 0) + { + if (al->len != bl->len) + return (int) al->len - (int) bl->len; + } + else + return res; + + an--; + bn--; + al = LEVEL_NEXT(al); + bl = LEVEL_NEXT(bl); + } + + return a->numlevel - b->numlevel; +} + +/* + * Returns a "distance" between a and b. If a < b, the distance is negative, + * consistent with the ltree_compare() ordering. + */ +float +ltree_compare_distance(const ltree *a, const ltree *b) +{ + ltree_level *al = LTREE_FIRST(a); + ltree_level *bl = LTREE_FIRST(b); + int an = a->numlevel; + int bn = b->numlevel; + + while (an > 0 && bn > 0) + { + int res; + + res = memcmp(al->name, bl->name, Min(al->len, bl->len)); + if (res == 0) { if (al->len != bl->len) - return (al->len - bl->len) * 10 * (an + 1); + return (float) (al->len - bl->len) * 10.0 * (an + 1); } else { if (res < 0) - res = -1; + return -1.0 * 10.0 * (an + 1); else - res = 1; - return res * 10 * (an + 1); + return 1.0 * 10.0 * (an + 1); } an--; @@ -74,7 +111,7 @@ ltree_compare(const ltree *a, const ltree *b) bl = LEVEL_NEXT(bl); } - return (a->numlevel - b->numlevel) * 10 * (an + 1); + return ((float) (a->numlevel - b->numlevel)) * 10.0 * (an + 1); } #define RUNCMP \ diff --git a/contrib/ltree/sql/ltree.sql b/contrib/ltree/sql/ltree.sql index c450ccdb43d..bad5647fb42 100644 --- a/contrib/ltree/sql/ltree.sql +++ b/contrib/ltree/sql/ltree.sql @@ -476,3 +476,9 @@ SELECT (repeat('x', 255) || repeat('|' || repeat('x', 255), 256))::lquery; --- Test for overflow of lquery_level.numvar, with a set of single-char --- variants in one level. SELECT (repeat('a|', 65535) || 'a')::lquery; + +-- Test that ltree_compare() does not overflow with very deep paths. +WITH s AS (SELECT 'a'::ltree AS v), + l AS (SELECT (repeat('a.', 14999) || 'a')::ltree AS v) +SELECT (l.v > s.v) AS gt_ok, (l.v < s.v) AS lt_ok, (l.v = s.v) AS eq_ok + FROM s, l; From 477efef089c31f7d260c923d64a2a75cb3aa580a Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 16 Jun 2026 15:58:17 +0900 Subject: [PATCH 074/250] pg_restore: Use dependency-based matching for STATISTICS DATA The previous approach introduced by 0dd93de69e80 was weak in terms of name matching, as an --index=foo could match with a table with the same name but from a different schema, pulling in more data than necessary. For example, imagine the following case: CREATE SCHEMA s1; CREATE SCHEMA s2; CREATE TABLE s1.foo (id int); INSERT INTO s1.foo SELECT generate_series(1,100); ANALYZE s1.foo; CREATE TABLE s2.bar (id int); CREATE INDEX foo ON s2.bar(id); INSERT INTO s2.bar SELECT generate_series(1,100); ANALYZE s2.bar; A targetted pg_restore --index=foo would grab the relation and attribute stats of s1.foo on top of the index s2.foo, which is incorrect. This commit fixes this scenario by relying on a lookup of the dependencies of a STATISTICS DATA TOC entry, checking if a TOC entry depends on an index or another relkind before matching with the names of the objects wanted for the restore. Discussion: https://postgr.es/m/ajDBwpxs-otl585H@paquier.xyz Backpatch-through: 18 --- src/bin/pg_dump/pg_backup_archiver.c | 39 +++++++++++++++++++++------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index c4e17359d9f..8f35bbb3779 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3190,17 +3190,36 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) bool dumpthis = false; /* - * Statistics data can be assigned for tables or indexes, so - * check both. + * Statistics data entries can be for tables or indexes. Check + * the parent dependency to determine which type this entry + * belongs to, then apply the appropriate name filter. */ - if (ropt->selTable && - (ropt->tableNames.head == NULL || - simple_string_list_member(&ropt->tableNames, te->tag))) - dumpthis = true; - if (ropt->selIndex && - (ropt->indexNames.head == NULL || - simple_string_list_member(&ropt->indexNames, te->tag))) - dumpthis = true; + for (int i = 0; i < te->nDeps; i++) + { + TocEntry *pte = getTocEntryByDumpId(AH, te->dependencies[i]); + + if (!pte) + continue; + + if (ropt->selTable && + (strcmp(pte->desc, "TABLE") == 0 || + strcmp(pte->desc, "VIEW") == 0 || + strcmp(pte->desc, "FOREIGN TABLE") == 0 || + strcmp(pte->desc, "MATERIALIZED VIEW") == 0)) + { + if (ropt->tableNames.head == NULL || + simple_string_list_member(&ropt->tableNames, pte->tag)) + dumpthis = true; + } + + if (ropt->selIndex && + strcmp(pte->desc, "INDEX") == 0) + { + if (ropt->indexNames.head == NULL || + simple_string_list_member(&ropt->indexNames, pte->tag)) + dumpthis = true; + } + } if (!dumpthis) return 0; } From bd7b2184390fb7ce4a37709a199990e55b251698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Tue, 16 Jun 2026 18:13:15 +0200 Subject: [PATCH 075/250] logical decoding: Correctly free speculative insertion The error path in ReorderBufferProcessTXN was not freeing (reorderbuffer.c's representation of) a speculative insertion record correctly. In assert-enabled builds, this leads to an assertion failure. In production builds, I see no effect; there may be a small transient leak, but in an improbable code path such as this, such a leak is not of any significance. For users running with assertions enabled, the crash is annoying. Fix by having ReorderBufferProcessTXN() free the speculative insert ahead of freeing the rest of the transaction, and no longer try to handle that insert as a separate argument to ReorderBufferResetTXN(). This code came in with commit 7259736a6e5b (14-era). Backpatch all the way back. In branches 14-16, also backpatch the assertion that originally fails in the problem scenario, which was added by dbed2e36625d (originally backpatched to 17), that at the end of ReorderBufferReturnTXN() the in-memory size of the transaction is zero. Author: Vishal Prasanna Reviewed-by: Hayato Kuroda Backpatch-through: 14 Discussion: https://postgr.es/m/19c7623e882.4080fd5426212.311756747309556767@zohocorp.com --- .../replication/logical/reorderbuffer.c | 22 +++++---- src/test/subscription/t/100_bugs.pl | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c index 11139a910b8..ead803171e8 100644 --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -2164,8 +2164,7 @@ static void ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, Snapshot snapshot_now, CommandId command_id, - XLogRecPtr last_lsn, - ReorderBufferChange *specinsert) + XLogRecPtr last_lsn) { /* Discard the changes that we just streamed */ ReorderBufferTruncateTXN(rb, txn, rbtxn_is_prepared(txn)); @@ -2173,13 +2172,6 @@ ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, /* Free all resources allocated for toast reconstruction */ ReorderBufferToastReset(rb, txn); - /* Return the spec insert change if it is not NULL */ - if (specinsert != NULL) - { - ReorderBufferFreeChange(rb, specinsert, true); - specinsert = NULL; - } - /* * For the streaming case, stop the stream and remember the command ID and * snapshot for the streaming run. @@ -2443,7 +2435,7 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, * CheckTableNotInUse() and locking. */ - /* clear out a pending (and thus failed) speculation */ + /* clear out a pending (= failed) speculative insertion */ if (specinsert != NULL) { ReorderBufferFreeChange(rb, specinsert, true); @@ -2753,6 +2745,13 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, if (using_subtxn) RollbackAndReleaseCurrentSubTransaction(); + /* Free the specinsert change before freeing the ReorderBufferTXN */ + if (specinsert != NULL) + { + ReorderBufferFreeChange(rb, specinsert, true); + specinsert = NULL; + } + /* * The error code ERRCODE_TRANSACTION_ROLLBACK indicates a concurrent * abort of the (sub)transaction we are streaming or preparing. We @@ -2786,8 +2785,7 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, /* Reset the TXN so that it is allowed to stream remaining data. */ ReorderBufferResetTXN(rb, txn, snapshot_now, - command_id, prev_lsn, - specinsert); + command_id, prev_lsn); } else { diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 50223054918..df6b2e1296d 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -605,4 +605,49 @@ BEGIN $node_publisher->stop('fast'); +# https://postgr.es/m/19c7623e882.4080fd5426212.311756747309556767%40zohocorp.com + +# The bug was that when an ERROR was raised while processing an INSERT ... ON +# CONFLICT statement, the decoded change misses to be free'd. This can cause an +# assertion failure if enabled. + +$node_publisher->rotate_logfile(); +$node_publisher->start(); + +# Create a publication with the zero-division row filter. It always throws an +# ERROR before publishing changes, when the filter is evaluated. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE tab_upsert (a INT PRIMARY KEY, b INT); + CREATE PUBLICATION pub_rowfilter_error FOR TABLE tab_upsert WHERE ((a / 0) > 0); + SELECT * FROM pg_create_logical_replication_slot('upsert_slot', 'pgoutput'); + INSERT INTO tab_upsert (a, b) VALUES (1, 1) + ON CONFLICT(a) DO UPDATE SET b = excluded.b; +)); + +# Decode the changes with a publication whose row filter causes a +# division by zero error, and verify that the logical decoder doesn't crash. +($ret, $stdout, $stderr) = $node_publisher->psql( + 'postgres', qq( + SELECT * + FROM pg_logical_slot_peek_binary_changes( + 'upsert_slot', + NULL, + NULL, + 'proto_version', '1', + 'publication_names', 'pub_rowfilter_error' + ); +)); + +ok( $stderr =~ qr/division by zero/, + 'peek logical changes with row filter causing division by zero throws error' +); + +# 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', "DROP TABLE tab_upsert"); + +$node_publisher->stop('fast'); + done_testing(); From 5a4fea0ce5d994479c8cdd0716816b3ae67a2070 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 17 Jun 2026 08:42:08 +0900 Subject: [PATCH 076/250] Fix another instability in recovery TAP test 004_timeline_switch The test did not wait for the standby to be connected to the primary. This breaks one assumption at the beginning of the test, where the primary is stopped to ensure that all its records are flushed to both standbys before moving on with its next steps. If standby_1 finishes ahead of standby_2, the test would be able work fine as the former waits for the latter. The opposite is not true, standby_2 getting ahead of standby_1 would cause the test to fail on timeout when standby_1 attempts to connect to standby_2. This commit adds an additional polling query after the two standbys are started, checking that both standbys are connected to the primary before processing with the initial steps of the test. Like 7185eddf0522, backpatch down to v14. Author: Sergey Tatarintsev Reviewed-by: Ewan Young Discussion: https://postgr.es/m/fea4190e-f8b5-4432-a52d-bcbee5f34366@postgrespro.ru Backpatch-through: 14 --- src/test/recovery/t/004_timeline_switch.pl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/recovery/t/004_timeline_switch.pl b/src/test/recovery/t/004_timeline_switch.pl index a6e2e2d4c37..d962b3adb1f 100644 --- a/src/test/recovery/t/004_timeline_switch.pl +++ b/src/test/recovery/t/004_timeline_switch.pl @@ -30,6 +30,10 @@ has_streaming => 1); $node_standby_2->start; +# Wait for standby_1 and standby_2 connection to the primary. +$node_primary->poll_query_until('postgres', + "SELECT count(1) = 2 FROM pg_stat_replication"); + # Create some content on primary $node_primary->safe_psql('postgres', "CREATE TABLE tab_int AS SELECT generate_series(1,1000) AS a"); From 13f940b4b56f38414a0dbd820f65c8e74f55d466 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 17 Jun 2026 16:05:37 +0900 Subject: [PATCH 077/250] Fix pgstat_count_io_op_time() calls passing incorrect information Several calls of pgstat_count_io_op_time() have been used as data to count negative values returned by pg_pread() or pg_pwrite(), leading to an incorrect count reported, casting them back to uint64. Most of the problematic calls updated here are adjusted so as we do not report buggy negative numbers anymore. In xlogrecovery.c, the spot updated still counts short reads. In xlog.c, after a WAL segment initialization, I/O numbers are aggregated only after checking that the operation has succeeded. issues introduced by a051e71e28a1. Reported-by: Peter Eisentraut Author: Bertrand Drouvot Reviewed-by: Michael Paquier Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/0db864e6-4477-4eba-b2be-d3523cc86564@eisentraut.org Backpatch-through: 18 --- src/backend/access/transam/xlog.c | 22 +++++++++++----------- src/backend/access/transam/xlogreader.c | 8 +++++--- src/backend/access/transam/xlogrecovery.c | 6 ++++-- src/backend/replication/walreceiver.c | 6 +++--- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 4f74400f3dd..e07cb910351 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -2434,9 +2434,6 @@ XLogWrite(XLogwrtRqst WriteRqst, TimeLineID tli, bool flexible) written = pg_pwrite(openLogFile, from, nleft, startoffset); pgstat_report_wait_end(); - pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_NORMAL, - IOOP_WRITE, start, 1, written); - if (written <= 0) { char xlogfname[MAXFNAMELEN]; @@ -2454,6 +2451,9 @@ XLogWrite(XLogwrtRqst WriteRqst, TimeLineID tli, bool flexible) errmsg("could not write to log file \"%s\" at offset %u, length %zu: %m", xlogfname, startoffset, nleft))); } + + pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_NORMAL, + IOOP_WRITE, start, 1, written); nleft -= written; from += written; startoffset += written; @@ -3276,14 +3276,6 @@ XLogFileInitInternal(XLogSegNo logsegno, TimeLineID logtli, } pgstat_report_wait_end(); - /* - * A full segment worth of data is written when using wal_init_zero. One - * byte is written when not using it. - */ - pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_INIT, IOOP_WRITE, - io_start, 1, - wal_init_zero ? wal_segment_size : 1); - if (save_errno) { /* @@ -3300,6 +3292,14 @@ XLogFileInitInternal(XLogSegNo logsegno, TimeLineID logtli, errmsg("could not write to file \"%s\": %m", tmppath))); } + /* + * A full segment worth of data is written when using wal_init_zero. One + * byte is written when not using it. + */ + pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_INIT, IOOP_WRITE, + io_start, 1, + wal_init_zero ? wal_segment_size : 1); + /* Measure I/O timing when flushing segment */ io_start = pgstat_prepare_io_time(track_wal_io_timing); diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c index 5c26d33a603..29d4df7a996 100644 --- a/src/backend/access/transam/xlogreader.c +++ b/src/backend/access/transam/xlogreader.c @@ -1578,9 +1578,6 @@ WALRead(XLogReaderState *state, #ifndef FRONTEND pgstat_report_wait_end(); - - pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_NORMAL, IOOP_READ, - io_start, 1, readbytes); #endif if (readbytes <= 0) @@ -1593,6 +1590,11 @@ WALRead(XLogReaderState *state, return false; } +#ifndef FRONTEND + pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_NORMAL, IOOP_READ, + io_start, 1, readbytes); +#endif + /* Update state for read */ recptr += readbytes; nbytes -= readbytes; diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index ff4d9edc790..476187113d4 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -3431,8 +3431,10 @@ XLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, int reqLen, pgstat_report_wait_end(); - pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_NORMAL, IOOP_READ, - io_start, 1, r); + /* Count I/O stats only for successful short reads */ + if (r > 0) + pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_NORMAL, IOOP_READ, + io_start, 1, r); XLogFileName(fname, curFileTLI, readSegNo, wal_segment_size); if (r < 0) diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c index 8c4d0fd9aed..6df19f89bef 100644 --- a/src/backend/replication/walreceiver.c +++ b/src/backend/replication/walreceiver.c @@ -931,9 +931,6 @@ XLogWalRcvWrite(char *buf, Size nbytes, XLogRecPtr recptr, TimeLineID tli) byteswritten = pg_pwrite(recvFile, buf, segbytes, (off_t) startoff); pgstat_report_wait_end(); - pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_NORMAL, - IOOP_WRITE, start, 1, byteswritten); - if (byteswritten <= 0) { char xlogfname[MAXFNAMELEN]; @@ -953,6 +950,9 @@ XLogWalRcvWrite(char *buf, Size nbytes, XLogRecPtr recptr, TimeLineID tli) xlogfname, startoff, (unsigned long) segbytes))); } + pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_NORMAL, + IOOP_WRITE, start, 1, byteswritten); + /* Update state for write */ recptr += byteswritten; From 7e085aabd5759c73d25556a5963866d5beaea5cd Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 17 Jun 2026 09:18:39 -0500 Subject: [PATCH 078/250] vacuumdb: Fix --missing-stats-only for partitioned indexes. The current form of the catalog query picks up partitioned tables with expression indexes that lack statistics. However, since such indexes never have statistics, there's no point in analyzing them. To fix, adjust the relevant part of the query to skip partitioned tables with expression indexes. While at it, remove the nearby stainherit check; entries for index expressions always have stainherit = false. Author: Baji Shaik Reviewed-by: Corey Huinker Discussion: https://postgr.es/m/CA%2Bfm-RPE1tEc6CUUPDyRbYTz9tF5Kw47nnk-Zq%3DyYvanbsxyCQ%40mail.gmail.com Backpatch-through: 18 --- src/bin/scripts/t/100_vacuumdb.pl | 1 + src/bin/scripts/vacuumdb.c | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl index ef2faf610b6..d8b89fc899c 100644 --- a/src/bin/scripts/t/100_vacuumdb.pl +++ b/src/bin/scripts/t/100_vacuumdb.pl @@ -322,6 +322,7 @@ $node->safe_psql('postgres', "CREATE TABLE regression_vacuumdb_parted (a INT) PARTITION BY LIST (a);\n" . "CREATE TABLE regression_vacuumdb_part1 PARTITION OF regression_vacuumdb_parted FOR VALUES IN (1);\n" + . "CREATE INDEX ON regression_vacuumdb_parted ((a + 1));\n" . "INSERT INTO regression_vacuumdb_parted VALUES (1);\n" . "ANALYZE regression_vacuumdb_part1;\n"); $node->issues_sql_like( diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c index e97ace18104..fa04109f2d7 100644 --- a/src/bin/scripts/vacuumdb.c +++ b/src/bin/scripts/vacuumdb.c @@ -985,10 +985,10 @@ retrieve_objects(PGconn *conn, vacuumingOptions *vacopts, " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" " AND NOT a.attisdropped\n" " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND NOT p.inherited\n" " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" - " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" - " AND s.stainherit OPERATOR(pg_catalog.=) p.inherited))\n"); + " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum))\n"); /* inheritance and regular stats */ appendPQExpBufferStr(&catalog_query, From e3b7a43fa9b6f7f5d33584080276bea66606b644 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 17 Jun 2026 11:04:41 -0400 Subject: [PATCH 079/250] jsonb_plperl, jsonb_plpython: Fix unguarded recursion and loops. Add check_stack_depth() to Jsonb_to_SV, SV_to_JsonbValue, PLyObject_FromJsonbContainer, and PLyObject_ToJsonbValue. Without this, deeply nested JSONB values can crash the backend with SIGSEGV instead of raising a proper error. Also add CHECK_FOR_INTERRUPTS() to the while loop in SV_to_JsonbValue that dereferences chains of Perl references, so that a circular reference (e.g. $x = \$x) can be cancelled by the user instead of spinning indefinitely. (We looked at detecting such circular references, but it seems more trouble than it's worth.) Author: Aleksander Alekseev Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAJ7c6TPbjkzUk4qJ5dHvDNEz0hBuFue3A-XWz_=897z+BC+z8A@mail.gmail.com Backpatch-through: 14 --- contrib/jsonb_plperl/jsonb_plperl.c | 15 +++++++++++++++ contrib/jsonb_plpython/jsonb_plpython.c | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/contrib/jsonb_plperl/jsonb_plperl.c b/contrib/jsonb_plperl/jsonb_plperl.c index c02e2d41af1..7375a66a6ab 100644 --- a/contrib/jsonb_plperl/jsonb_plperl.c +++ b/contrib/jsonb_plperl/jsonb_plperl.c @@ -3,6 +3,7 @@ #include #include "fmgr.h" +#include "miscadmin.h" #include "plperl.h" #include "utils/fmgrprotos.h" #include "utils/jsonb.h" @@ -66,6 +67,9 @@ Jsonb_to_SV(JsonbContainer *jsonb) JsonbIterator *it; JsonbIteratorToken r; + /* this can recurse via JsonbValue_to_SV() */ + check_stack_depth(); + it = JsonbIteratorInit(jsonb); r = JsonbIteratorNext(&it, &v, true); @@ -179,9 +183,20 @@ SV_to_JsonbValue(SV *in, JsonbParseState **jsonb_state, bool is_elem) dTHX; JsonbValue out; /* result */ + /* this can recurse via AV_to_JsonbValue() or HV_to_JsonbValue() */ + check_stack_depth(); + /* Dereference references recursively. */ while (SvROK(in)) + { + /* + * It's possible for circular references to make this an infinite + * loop. Checking for such a situation seems like much more trouble + * than it's worth, but let's provide a way to break out of the loop. + */ + CHECK_FOR_INTERRUPTS(); in = SvRV(in); + } switch (SvTYPE(in)) { diff --git a/contrib/jsonb_plpython/jsonb_plpython.c b/contrib/jsonb_plpython/jsonb_plpython.c index 9383615abbf..ef5a3d3ead3 100644 --- a/contrib/jsonb_plpython/jsonb_plpython.c +++ b/contrib/jsonb_plpython/jsonb_plpython.c @@ -1,5 +1,6 @@ #include "postgres.h" +#include "miscadmin.h" #include "plpy_elog.h" #include "plpy_typeio.h" #include "plpy_util.h" @@ -141,6 +142,9 @@ PLyObject_FromJsonbContainer(JsonbContainer *jsonb) JsonbIterator *it; PyObject *result; + /* this can recurse via PLyObject_FromJsonbValue() */ + check_stack_depth(); + it = JsonbIteratorInit(jsonb); r = JsonbIteratorNext(&it, &v, true); @@ -411,6 +415,9 @@ PLyObject_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state, bool is_ele { JsonbValue *out; + /* this can recurse via PLyMapping_ToJsonbValue() */ + check_stack_depth(); + if (!PyUnicode_Check(obj)) { if (PySequence_Check(obj)) From 1fb397f730b209edaff995f9cc411a5d8ed2d65f Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Wed, 17 Jun 2026 09:57:15 -0700 Subject: [PATCH 080/250] oauth_validator: Print captured stderr after call-count failure If the call count test fails, you'll reasonably want to know what the network trace looked like, but that information is currently swallowed. Print it out instead. Backpatch-through: 18 --- src/test/modules/oauth_validator/t/001_server.pl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl index c0dafb8be76..95693fcdb18 100644 --- a/src/test/modules/oauth_validator/t/001_server.pl +++ b/src/test/modules/oauth_validator/t/001_server.pl @@ -444,7 +444,10 @@ sub connstr # to change across OSes and Curl updates, we're likely in trouble if we see # hundreds or thousands of calls. $stderr =~ $count_pattern; - cmp_ok($1, '<', 100, "call count is reasonably small"); + unless (cmp_ok($1, '<', 100, "call count is reasonably small")) + { + diag "full stderr:\n$stderr"; + } } # Stress test: make sure our builtin flow operates correctly even if the client From 357e4d64f871cccd57390581070c4239465c2eb5 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Wed, 17 Jun 2026 09:57:20 -0700 Subject: [PATCH 081/250] libpq-oauth: Print libcurl version with OAUTHDEBUG_UNSAFE_TRACE When debugging an OAuth trace, it's helpful to know what version of Curl is in use. The SSL library that Curl is using (which may not be the one in use by libpq) is also relevant, and it's just as easy to get, so print that too. This is being added post-feature-freeze, with RMT approval, in order to fix some tests in the face of an upstream Curl regression. A subsequent commit will make use of it in oauth_validator. Backpatch to 18 as well. Tested-by: Tom Lane Discussion: https://postgr.es/m/CAOYmi%2B%3DkP86t%2BZFFXNQ9G6K4ht7utdmB%3DCzhP%3DZ2wvuBymOTtQ%40mail.gmail.com Backpatch-through: 18 --- src/interfaces/libpq-oauth/oauth-curl.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c index 64b5306372a..70fac3ea243 100644 --- a/src/interfaces/libpq-oauth/oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -2666,9 +2666,7 @@ initialize_curl(PGconn *conn) * PG_BOOL_YES/NO in cases where that's not the final answer. */ static volatile PGTernaryBool init_successful = PG_BOOL_UNKNOWN; -#if HAVE_THREADSAFE_CURL_GLOBAL_INIT curl_version_info_data *info; -#endif #if !HAVE_THREADSAFE_CURL_GLOBAL_INIT @@ -2716,6 +2714,8 @@ initialize_curl(PGconn *conn) goto done; } + info = curl_version_info(CURLVERSION_NOW); + #if HAVE_THREADSAFE_CURL_GLOBAL_INIT /* @@ -2725,7 +2725,6 @@ initialize_curl(PGconn *conn) * situation), then double-check to make sure the runtime setting agrees, * to try to catch silent downgrades. */ - info = curl_version_info(CURLVERSION_NOW); if (!(info->features & CURL_VERSION_THREADSAFE)) { /* @@ -2742,6 +2741,22 @@ initialize_curl(PGconn *conn) } #endif + if (oauth_unsafe_debugging_enabled()) + { + /* + * Record the version of libcurl and its SSL library when tracing, + * since those are likely to be relevant to network debugging. Neither + * of these strings should be NULL in a useful installation, but + * that's no reason to crash if they are, so provide fallbacks. + * + * Other Curl dependency info might be helpful in the future, too; + * just be sure to check info->age as needed when adding more. + */ + fprintf(stderr, "[libpq] initialized libcurl %s (%s)\n", + info->version ? info->version : "version unknown", + info->ssl_version ? info->ssl_version : "no SSL"); + } + init_successful = PG_BOOL_YES; done: From c5c35fd7c55cfc3ad072d131bff7472dd49a97b2 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Wed, 17 Jun 2026 09:57:59 -0700 Subject: [PATCH 082/250] oauth: Skip call-count test for libcurl 8.20.0 The call-count test in 001_server.pl runs into a recent upstream regression in Curl: https://github.com/curl/curl/issues/21547 The symptom is high CPU usage on some platforms during OAuth HTTP requests. But it looks like the fix is on track for a June 2026 release, as part of Curl 8.21.0, so just skip the test if we happen to be using the broken version. Reported-by: Andrew Dunstan Reported-by: Tom Lane Tested-by: Tom Lane Discussion: https://postgr.es/m/CAOYmi%2B%3DyrwMSsHuNJ1V14isA4iSix5Xb3P3VEp1X0BS61MdV4A%40mail.gmail.com Backpatch-through: 18 --- .../modules/oauth_validator/t/001_server.pl | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl index 95693fcdb18..295342abc69 100644 --- a/src/test/modules/oauth_validator/t/001_server.pl +++ b/src/test/modules/oauth_validator/t/001_server.pl @@ -436,17 +436,27 @@ sub connstr qr@Visit https://example\.com/ and enter the code: postgresuser@, "call count: stderr matches"); -my $count_pattern = qr/\[libpq\] total number of polls: (\d+)/; -if (like($stderr, $count_pattern, "call count: count is printed")) +SKIP: { - # For reference, a typical flow with two retries might take between 5-15 - # calls to the client implementation. And while this will probably continue - # to change across OSes and Curl updates, we're likely in trouble if we see - # hundreds or thousands of calls. - $stderr =~ $count_pattern; - unless (cmp_ok($1, '<', 100, "call count is reasonably small")) + # Curl 8.20.0 regressed this test case: + # + # https://github.com/curl/curl/issues/21547 + # + skip 'call-count test is known to fail with libcurl 8.20.0', 2 + if $stderr =~ m/\Qinitialized libcurl 8.20.0\E/; + + my $count_pattern = qr/\[libpq\] total number of polls: (\d+)/; + if (like($stderr, $count_pattern, "call count: count is printed")) { - diag "full stderr:\n$stderr"; + # For reference, a typical flow with two retries might take between 5-15 + # calls to the client implementation. And while this will probably + # continue to change across OSes and Curl updates, we're likely in + # trouble if we see hundreds or thousands of calls. + $stderr =~ $count_pattern; + unless (cmp_ok($1, '<', 100, "call count is reasonably small")) + { + diag "full stderr:\n$stderr"; + } } } From 5cc59834b860ed48d710c1baa9c50c66540c64d0 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 18 Jun 2026 11:49:34 +0900 Subject: [PATCH 083/250] Fix PANIC with track_functions due to concurrent drop of pgstats entries pgstat_drop_entry_internal() generates an ERROR if facing a pgstats entry already marked as dropped. With a workload doing a lot of concurrent CALL and DROP/CREATE PROCEDURE, it could be possible for AtEOXact_PgStat_DroppedStats(), that wants to do transactional drops, to find entries that are already dropped, after a commit record has been written. In this case, ERRORs are upgraded to PANIC, taking down the server. This issue is fixed by making pgstat_drop_entry() optionally more tolerant to concurrent drops, adding to the routine a missing_ok option to make some of its callers more tolerant (spoiler: some of the callers want a strict behavior, like replication slots and backend stats). pgstat_drop_entry_internal() cannot be called anymore for an entry marked as dropped, hence its error is replaced by an assertion. Functions are handled as a special case in core; this problem could also apply to custom stats kinds depending on what an extension does. track_functions is costly when enabled (disabled by default), which is perhaps the main reason why this has not be found yet. A similar version of this patch has been proposed by Sami Imseih on a different thread for a feature in development. This version has tweaked here by me for the sake of fixing this issue. Reported-by: zhanglihui Author: Sami Imseih Author: Michael Paquier Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/19520-73873648d44793cf@postgresql.org Backpatch-through: 15 --- src/backend/utils/activity/pgstat.c | 2 +- src/backend/utils/activity/pgstat_function.c | 2 +- src/backend/utils/activity/pgstat_replslot.c | 2 +- src/backend/utils/activity/pgstat_shmem.c | 28 +++++++++++++------ src/backend/utils/activity/pgstat_xact.c | 8 +++--- src/include/utils/pgstat_internal.h | 3 +- .../injection_points/injection_stats.c | 2 +- 7 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index 9113ee9d6a9..d997be2e1d9 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -620,7 +620,7 @@ pgstat_shutdown_hook(int code, Datum arg) dlist_init(&pgStatPending); /* drop the backend stats entry */ - if (!pgstat_drop_entry(PGSTAT_KIND_BACKEND, InvalidOid, MyProcNumber)) + if (!pgstat_drop_entry(PGSTAT_KIND_BACKEND, InvalidOid, MyProcNumber, false)) pgstat_request_entry_refs_gc(); pgstat_detach_shmem(); diff --git a/src/backend/utils/activity/pgstat_function.c b/src/backend/utils/activity/pgstat_function.c index 6214f93d36e..f763a0d6f5b 100644 --- a/src/backend/utils/activity/pgstat_function.c +++ b/src/backend/utils/activity/pgstat_function.c @@ -113,7 +113,7 @@ pgstat_init_function_usage(FunctionCallInfo fcinfo, if (!SearchSysCacheExists1(PROCOID, ObjectIdGetDatum(fcinfo->flinfo->fn_oid))) { pgstat_drop_entry(PGSTAT_KIND_FUNCTION, MyDatabaseId, - fcinfo->flinfo->fn_oid); + fcinfo->flinfo->fn_oid, true); ereport(ERROR, errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("function call to dropped function")); } diff --git a/src/backend/utils/activity/pgstat_replslot.c b/src/backend/utils/activity/pgstat_replslot.c index ccfb11c49bf..746b97a0b5a 100644 --- a/src/backend/utils/activity/pgstat_replslot.c +++ b/src/backend/utils/activity/pgstat_replslot.c @@ -158,7 +158,7 @@ pgstat_drop_replslot(ReplicationSlot *slot) Assert(LWLockHeldByMeInMode(ReplicationSlotAllocationLock, LW_EXCLUSIVE)); if (!pgstat_drop_entry(PGSTAT_KIND_REPLSLOT, InvalidOid, - ReplicationSlotIndex(slot))) + ReplicationSlotIndex(slot), false)) pgstat_request_entry_refs_gc(); } diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c index 2b7f783ef7c..414b66be2dc 100644 --- a/src/backend/utils/activity/pgstat_shmem.c +++ b/src/backend/utils/activity/pgstat_shmem.c @@ -898,14 +898,7 @@ pgstat_drop_entry_internal(PgStatShared_HashEntry *shent, * Signal that the entry is dropped - this will eventually cause other * backends to release their references. */ - if (shent->dropped) - elog(ERROR, - "trying to drop stats entry already dropped: kind=%s dboid=%u objid=%" PRIu64 " refcount=%u generation=%u", - pgstat_get_kind_info(shent->key.kind)->name, - shent->key.dboid, - shent->key.objid, - pg_atomic_read_u32(&shent->refcount), - pg_atomic_read_u32(&shent->generation)); + Assert(!shent->dropped); shent->dropped = true; /* release refcount marking entry as not dropped */ @@ -981,13 +974,16 @@ pgstat_drop_database_and_contents(Oid dboid) * This routine returns false if the stats entry of the dropped object could * not be freed, true otherwise. * + * If missing_ok is true, skip entries that have been concurrently dropped. + * * The callers of this function should call pgstat_request_entry_refs_gc() * if the stats entry could not be freed, to ensure that this entry's memory * can be reclaimed later by a different backend calling * pgstat_gc_entry_refs(). */ bool -pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid) +pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid, + bool missing_ok) { PgStat_HashKey key; PgStatShared_HashEntry *shent; @@ -1015,6 +1011,20 @@ pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid) shent = dshash_find(pgStatLocal.shared_hash, &key, true); if (shent) { + if (shent->dropped) + { + if (!missing_ok) + elog(ERROR, + "trying to drop stats entry already dropped: kind=%s dboid=%u objid=%" PRIu64 " refcount=%u generation=%u", + pgstat_get_kind_info(shent->key.kind)->name, + shent->key.dboid, + shent->key.objid, + pg_atomic_read_u32(&shent->refcount), + pg_atomic_read_u32(&shent->generation)); + dshash_release_lock(pgStatLocal.shared_hash, shent); + return true; + } + freed = pgstat_drop_entry_internal(shent, NULL); /* diff --git a/src/backend/utils/activity/pgstat_xact.c b/src/backend/utils/activity/pgstat_xact.c index bc9864bd8d9..b058c978dcf 100644 --- a/src/backend/utils/activity/pgstat_xact.c +++ b/src/backend/utils/activity/pgstat_xact.c @@ -85,7 +85,7 @@ AtEOXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, bool isCommit) * Transaction that dropped an object committed. Drop the stats * too. */ - if (!pgstat_drop_entry(it->kind, it->dboid, objid)) + if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) not_freed_count++; } else if (!isCommit && pending->is_create) @@ -94,7 +94,7 @@ AtEOXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, bool isCommit) * Transaction that created an object aborted. Drop the stats * associated with the object. */ - if (!pgstat_drop_entry(it->kind, it->dboid, objid)) + if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) not_freed_count++; } @@ -160,7 +160,7 @@ AtEOSubXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, * Subtransaction creating a new stats object aborted. Drop the * stats object. */ - if (!pgstat_drop_entry(it->kind, it->dboid, objid)) + if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) not_freed_count++; pfree(pending); } @@ -323,7 +323,7 @@ pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, xl_xact_stats_item *it = &items[i]; uint64 objid = ((uint64) it->objid_hi) << 32 | it->objid_lo; - if (!pgstat_drop_entry(it->kind, it->dboid, objid)) + if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) not_freed_count++; } diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index 6cf00008f63..4f840c38f07 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -708,7 +708,8 @@ extern PgStat_EntryRef *pgstat_get_entry_ref(PgStat_Kind kind, Oid dboid, uint64 extern bool pgstat_lock_entry(PgStat_EntryRef *entry_ref, bool nowait); extern bool pgstat_lock_entry_shared(PgStat_EntryRef *entry_ref, bool nowait); extern void pgstat_unlock_entry(PgStat_EntryRef *entry_ref); -extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid); +extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid, + bool missing_ok); extern void pgstat_drop_all_entries(void); extern void pgstat_drop_matching_entries(bool (*do_drop) (PgStatShared_HashEntry *, Datum), Datum match_data); diff --git a/src/test/modules/injection_points/injection_stats.c b/src/test/modules/injection_points/injection_stats.c index ca8df4ad217..435a0484a4b 100644 --- a/src/test/modules/injection_points/injection_stats.c +++ b/src/test/modules/injection_points/injection_stats.c @@ -150,7 +150,7 @@ pgstat_drop_inj(const char *name) return; if (!pgstat_drop_entry(PGSTAT_KIND_INJECTION, InvalidOid, - PGSTAT_INJ_IDX(name))) + PGSTAT_INJ_IDX(name), false)) pgstat_request_entry_refs_gc(); } From 08458bcaea5bc893c6150cc86adbac259a5d7b36 Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Thu, 18 Jun 2026 09:42:56 +0530 Subject: [PATCH 084/250] Avoid stale slot access after dropping obsolete synced slots. drop_local_obsolete_slots() continued to dereference local_slot after calling ReplicationSlotDropAcquired(). Once the slot is dropped, its entry in the slot array can be reused by another backend, so later reads of local_slot->data could observe a different slot's name or database OID, leading to an incorrect unlock and log message. Save the slot name and database OID before performing the drop, and use the saved values for the subsequent UnlockSharedObject() call and the log message. While at it, emit the "dropped replication slot" message only when a slot was actually dropped, rather than unconditionally. Author: Xuneng Zhou Reviewed-by: Zhijie Hou Reviewed-by: Amit Kapila Reviewed-by: Fujii Masao Backpatch-through: 17, where it was introduced Discussion: https://postgr.es/m/TY4PR01MB177184FF9EE916F577E1F554194082@TY4PR01MB17718.jpnprd01.prod.outlook.com --- src/backend/replication/logical/slotsync.c | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c index bc42d74fec2..c4dda8aa5f1 100644 --- a/src/backend/replication/logical/slotsync.c +++ b/src/backend/replication/logical/slotsync.c @@ -463,6 +463,7 @@ drop_local_obsolete_slots(List *remote_slot_list) /* Drop the local slot if it is not required to be retained. */ if (!local_sync_slot_required(local_slot, remote_slot_list)) { + Oid slot_database = local_slot->data.database; bool synced_slot; /* @@ -470,8 +471,8 @@ drop_local_obsolete_slots(List *remote_slot_list) * ReplicationSlotsDropDBSlots(), trying to drop the same slot * during a drop-database operation. */ - LockSharedObject(DatabaseRelationId, local_slot->data.database, - 0, AccessShareLock); + LockSharedObject(DatabaseRelationId, slot_database, 0, + AccessShareLock); /* * In the small window between getting the slot to drop and @@ -488,17 +489,19 @@ drop_local_obsolete_slots(List *remote_slot_list) if (synced_slot) { - ReplicationSlotAcquire(NameStr(local_slot->data.name), true, false); + NameData slot_name = local_slot->data.name; + + ReplicationSlotAcquire(NameStr(slot_name), true, false); ReplicationSlotDropAcquired(); - } - UnlockSharedObject(DatabaseRelationId, local_slot->data.database, - 0, AccessShareLock); + ereport(LOG, + errmsg("dropped replication slot \"%s\" of database with OID %u", + NameStr(slot_name), + slot_database)); + } - ereport(LOG, - errmsg("dropped replication slot \"%s\" of database with OID %u", - NameStr(local_slot->data.name), - local_slot->data.database)); + UnlockSharedObject(DatabaseRelationId, slot_database, 0, + AccessShareLock); } } } From 8a4f389ddce54c5b9bc1f0dcc99e9f74944be813 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 18 Jun 2026 14:48:37 +0900 Subject: [PATCH 085/250] Update .abi-compliance-history for pgstat_drop_entry() As noted in the commit message of 850b9218c8e4, this function has gained an extra called "missing_ok". All the callers of this routine should be in core in the v15-v17 range. For v18, I have found one custom stats kind that would be impacted by this change. Discussion: https://postgr.es/m/ajOE3uRxVgSlPRcw@paquier.xyz Backpatch-through: 15-18 --- .abi-compliance-history | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.abi-compliance-history b/.abi-compliance-history index a4bf3336cac..6ba3e7519b7 100644 --- a/.abi-compliance-history +++ b/.abi-compliance-history @@ -18,6 +18,15 @@ # Be sure to replace "" with details of your change and # why it is deemed acceptable. +5cc59834b860ed48d710c1baa9c50c66540c64d0 +# +# Fix PANIC with track_functions due to concurrent drop of pgstats entries +# 2026-06-18 11:49:34 +0900 +# +# This commit has added a "missing_ok" argument to pgstat_drop_entry(). All +# the callers of this routine are in core for v15-v17. One custom stats kinds +# available in the public since v18 is impacted (maintainer informed). + 8d9a97e0bb6d820dac553848f0d5d8cc3f3e219d # # Avoid name collision with NOT NULL constraints From 5562b2657f942113b77bd557234a2ac4d08a49c5 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Thu, 18 Jun 2026 09:31:27 -0500 Subject: [PATCH 086/250] doc: Fix "Prev" link, take 2. Commit 6678b58d78 fixed a wrong "Prev" link by changing the link generation code to use [position()=last()] instead of [last()] in the predicate on the union of reverse axes. Unfortunately, that caused documentation builds to take much longer. To fix, combine the "preceding" and "ancestor" steps into one "preceding" step and one "ancestor" step, and revert the predicate back to [last()]. The smaller union evades the libxml2 bug while avoiding the build time regression. Reported-by: Tom Lane Tested-by: Tom Lane Discussion: https://postgr.es/m/1132496.1781718007%40sss.pgh.pa.us Backpatch-through: 14 --- doc/src/sgml/stylesheet-speedup-xhtml.xsl | 52 +++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/doc/src/sgml/stylesheet-speedup-xhtml.xsl b/doc/src/sgml/stylesheet-speedup-xhtml.xsl index a3b3692ba03..0e8e97c7e5c 100644 --- a/doc/src/sgml/stylesheet-speedup-xhtml.xsl +++ b/doc/src/sgml/stylesheet-speedup-xhtml.xsl @@ -183,32 +183,32 @@ + select="(preceding::*[self::book + or self::preface + or self::chapter + or self::appendix + or self::part + or self::reference + or self::refentry + or self::colophon + or self::article + or self::topic + or self::sect1 + or self::bibliography[parent::article or parent::book or parent::part] + or self ::glossary[parent::article or parent::book or parent::part] + or self::index[$generate.index != 0] + [parent::article or parent::book or parent::part] + or self::setindex[$generate.index != 0]][1] + |ancestor::*[self::set + or self::book + or self::preface + or self::chapter + or self::appendix + or self::part + or self::reference + or self::article + or self::topic + or self::sect1][1])[last()]"/> Date: Thu, 18 Jun 2026 11:29:49 -0500 Subject: [PATCH 088/250] Silence "may be used uninitialized" compiler warning. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Newer gcc warns that this "actual_arg_types" variable may be used uninitialized, but visual inspection indicates there's no bug. To silence the warning, initialize the variable to zeros. Bug: #19485 Reported-by: Hans Buschmann Tested-by: Erik Rijkers Tested-by: Hans Buschmann Reviewed-by: Tristan Partin Reviewed-by: Álvaro Herrera Discussion: https://postgr.es/m/19485-2b03231a775756f1%40postgresql.org Discussion: https://postgr.es/m/6c52a1a6612948519468d46cb224a8c4%40nidsa.net --- src/backend/optimizer/util/clauses.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index c1dc8644be9..3a2d753907b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -4389,7 +4389,7 @@ recheck_cast_function_args(List *args, Oid result_type, { Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple); int nargs; - Oid actual_arg_types[FUNC_MAX_ARGS]; + Oid actual_arg_types[FUNC_MAX_ARGS] = {0}; Oid declared_arg_types[FUNC_MAX_ARGS]; Oid rettype; ListCell *lc; From e9692de1d6f617031be73251949eae31b29275bd Mon Sep 17 00:00:00 2001 From: David Rowley Date: Fri, 19 Jun 2026 15:26:51 +1200 Subject: [PATCH 089/250] Update JIT tuple deforming code for virtual generated columns The JIT deforming code contains an optimization that determines which columns are guaranteed to exist in the tuple. That's used to allow skipping of reading the tuple's natts when the code only needs to deform attributes that are guaranteed to always exist in all tuples. 83ea6c540 missed updating this code to account for VIRTUAL generated columns. These are stored as NULLs in the tuple, but may be defined as NOT NULL. This could result in the code thinking more columns are guaranteed to exist than actually do. Author: David Rowley Reviewed-by: Chao Li Backpatch-through: 18 Discussion: https://postgr.es/m/1151393.1781734980@sss.pgh.pa.us --- src/backend/jit/llvm/llvmjit_deform.c | 36 ++++++++++------ .../regress/expected/generated_virtual.out | 42 +++++++++++++++++++ src/test/regress/sql/generated_virtual.sql | 21 ++++++++++ 3 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/backend/jit/llvm/llvmjit_deform.c b/src/backend/jit/llvm/llvmjit_deform.c index 798c1ce4ed2..9cff932d058 100644 --- a/src/backend/jit/llvm/llvmjit_deform.c +++ b/src/backend/jit/llvm/llvmjit_deform.c @@ -105,27 +105,32 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, funcname = llvm_expand_funcname(context, "deform"); /* - * Check which columns have to exist, so we don't have to check the row's - * natts unnecessarily. + * Check which columns have to exist in all tuples, so we don't have to + * check the row's natts unnecessarily. */ for (attnum = 0; attnum < desc->natts; attnum++) { - CompactAttribute *att = TupleDescCompactAttr(desc, attnum); + CompactAttribute *catt = TupleDescCompactAttr(desc, attnum); + Form_pg_attribute attr = TupleDescAttr(desc, attnum); /* * If the column is declared NOT NULL then it must be present in every * tuple, unless there's a "missing" entry that could provide a * non-NULL value for it. That in turn guarantees that the NULL bitmap * - if there are any NULLable columns - is at least long enough to - * cover columns up to attnum. + * cover columns up to attnum. We treat virtual generated columns + * similar to atthasmissing columns, as these columns could either not + * be represented in the tuple or could have the column represented as + * a NULL in the null bitmap. * * Be paranoid and also check !attisdropped, even though the * combination of attisdropped && attnotnull combination shouldn't * exist. */ - if (att->attnullability == ATTNULLABLE_VALID && - !att->atthasmissing && - !att->attisdropped) + if (catt->attnullability == ATTNULLABLE_VALID && + !catt->atthasmissing && + !catt->attisdropped && + attr->attgenerated != ATTRIBUTE_GENERATED_VIRTUAL) guaranteed_column_number = attnum; } @@ -394,6 +399,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, for (attnum = 0; attnum < natts; attnum++) { CompactAttribute *att = TupleDescCompactAttr(desc, attnum); + Form_pg_attribute attr = TupleDescAttr(desc, attnum); + LLVMValueRef v_incby; int alignto = att->attalignby; LLVMValueRef l_attno = l_int16_const(lc, attnum); @@ -436,9 +443,12 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, /* * Check for nulls if necessary. No need to take missing attributes * into account, because if they're present the heaptuple's natts - * would have indicated that a slot_getmissingattrs() is needed. + * would have indicated that a slot_getmissingattrs() is needed. When + * present in the tuple, virtual generated columns are always stored + * as NULL, so we must always perform NULL checks for these. */ - if (att->attnullability != ATTNULLABLE_VALID) + if (att->attnullability != ATTNULLABLE_VALID || + attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) { LLVMBasicBlockRef b_ifnotnull; LLVMBasicBlockRef b_ifnull; @@ -616,12 +626,14 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, known_alignment += att->attlen; } else if (att->attnullability == ATTNULLABLE_VALID && + attr->attgenerated != ATTRIBUTE_GENERATED_VIRTUAL && (att->attlen % alignto) == 0) { /* - * After a NOT NULL fixed-width column with a length that is a - * multiple of its alignment requirement, we know the following - * column is aligned to at least the current column's alignment. + * After a NOT NULL (and not virtual generated) fixed-width column + * with a length that is a multiple of its alignment requirement, + * we know the following column is aligned to at least the current + * column's alignment. */ Assert(att->attlen > 0); known_alignment = alignto; diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out index 171d68ba2e2..03698488907 100644 --- a/src/test/regress/expected/generated_virtual.out +++ b/src/test/regress/expected/generated_virtual.out @@ -700,6 +700,48 @@ ERROR: null value in column "b" of relation "gtest21b" violates not-null constr DETAIL: Failing row contains (null, virtual). ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; INSERT INTO gtest21b (a) VALUES (0); -- ok now +-- virtual generated columns are not physically stored, even when not null +CREATE TABLE gtest21c (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL NOT NULL, c int NOT NULL); +INSERT INTO gtest21c (a, c) VALUES (10, 42); +SELECT a, b, c FROM gtest21c; + a | b | c +----+----+---- + 10 | 20 | 42 +(1 row) + +DROP TABLE gtest21c; +-- try adding a virtual generated column to an existing table with tuples, +-- then try adding an atthasmissing column before adding a normal nullable +-- column. +CREATE TABLE gtest21d (a int NOT NULL); +INSERT INTO gtest21d (a) VALUES(10); +ALTER TABLE gtest21d ADD COLUMN b INT GENERATED ALWAYS AS (a * 10) VIRTUAL NOT NULL; +SELECT * FROM gtest21d ORDER BY a; + a | b +----+----- + 10 | 100 +(1 row) + +INSERT INTO gtest21d (a) VALUES(20); +ALTER TABLE gtest21d ADD COLUMN c INT NOT NULL DEFAULT 1234; +SELECT * FROM gtest21d ORDER BY a; + a | b | c +----+-----+------ + 10 | 100 | 1234 + 20 | 200 | 1234 +(2 rows) + +ALTER TABLE gtest21d ADD COLUMN d INT; +INSERT INTO gtest21d (a, c, d) VALUES(30, 12345, 100); +SELECT * FROM gtest21d ORDER BY a; + a | b | c | d +----+-----+-------+----- + 10 | 100 | 1234 | + 20 | 200 | 1234 | + 30 | 300 | 12345 | 100 +(3 rows) + +DROP TABLE gtest21d; -- not-null constraint with partitioned table CREATE TABLE gtestnn_parent ( f1 int, diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql index 0ba8c626047..6054a8c7125 100644 --- a/src/test/regress/sql/generated_virtual.sql +++ b/src/test/regress/sql/generated_virtual.sql @@ -366,6 +366,27 @@ INSERT INTO gtest21b (a) VALUES (NULL); -- error ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; INSERT INTO gtest21b (a) VALUES (0); -- ok now +-- virtual generated columns are not physically stored, even when not null +CREATE TABLE gtest21c (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL NOT NULL, c int NOT NULL); +INSERT INTO gtest21c (a, c) VALUES (10, 42); +SELECT a, b, c FROM gtest21c; +DROP TABLE gtest21c; + +-- try adding a virtual generated column to an existing table with tuples, +-- then try adding an atthasmissing column before adding a normal nullable +-- column. +CREATE TABLE gtest21d (a int NOT NULL); +INSERT INTO gtest21d (a) VALUES(10); +ALTER TABLE gtest21d ADD COLUMN b INT GENERATED ALWAYS AS (a * 10) VIRTUAL NOT NULL; +SELECT * FROM gtest21d ORDER BY a; +INSERT INTO gtest21d (a) VALUES(20); +ALTER TABLE gtest21d ADD COLUMN c INT NOT NULL DEFAULT 1234; +SELECT * FROM gtest21d ORDER BY a; +ALTER TABLE gtest21d ADD COLUMN d INT; +INSERT INTO gtest21d (a, c, d) VALUES(30, 12345, 100); +SELECT * FROM gtest21d ORDER BY a; +DROP TABLE gtest21d; + -- not-null constraint with partitioned table CREATE TABLE gtestnn_parent ( f1 int, From aa80c34c85f4eb81d515f341b663bd196d18837c Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Fri, 19 Jun 2026 12:52:00 -0400 Subject: [PATCH 090/250] Make pg_mkdir_p() tolerant of a concurrent directory creation. pg_mkdir_p creates each missing path component with a stat() followed by mkdir(). If the stat() reports the component as absent but another process creates it in the window before this process's mkdir(), mkdir() fails with EEXIST and pg_mkdir_p treated that as a hard error -- unlike "mkdir -p", which is meant to be idempotent and race-tolerant. This shows up when several processes concurrently create paths that share an ancestor directory: for example, parallel initdb runs whose data directories live under a common temporary directory. One process wins the race to create the shared ancestor and the others fail with could not create directory "...": File exists Fix this race condition by first trying mkdir() and only attempting stat() if it fails with EEXIST. On Windows, there's an additional problem: stat() opens a file handle and participates in share-mode locking, which means it can transiently fail on a directory another process is concurrently creating. Use GetFileAttributes() instead: it requests only FILE_READ_ATTRIBUTES and is exempt from share-mode denial, so it reliably sees a concurrently-created directory. I (tgl) also chose to back-patch 039f7ee0f's effects on this function, so that pgmkdirp.c remains identical in all live branches. Author: Andrew Dunstan Co-authored-by: Tom Lane Discussion: https://postgr.es/m/3ca004de-e49b-4471-b8aa-fd656e70f68c@dunslane.net Backpatch-through: 14 --- src/port/pgmkdirp.c | 49 ++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/port/pgmkdirp.c b/src/port/pgmkdirp.c index d943559760d..3e6b06fce79 100644 --- a/src/port/pgmkdirp.c +++ b/src/port/pgmkdirp.c @@ -56,7 +56,6 @@ int pg_mkdir_p(char *path, int omode) { - struct stat sb; mode_t numask, oumask; int last, @@ -73,7 +72,7 @@ pg_mkdir_p(char *path, int omode) if (p[0] == '/' && p[1] == '/') { /* network drive */ - p = strstr(p + 2, "/"); + p = strchr(p + 2, '/'); if (p == NULL) { errno = EINVAL; @@ -119,24 +118,46 @@ pg_mkdir_p(char *path, int omode) if (last) (void) umask(oumask); - /* check for pre-existing directory */ - if (stat(path, &sb) == 0) + if (mkdir(path, last ? omode : S_IRWXU | S_IRWXG | S_IRWXO) < 0) { - if (!S_ISDIR(sb.st_mode)) + /* + * If we got EEXIST because there's already a directory there, + * don't complain. + */ +#ifndef WIN32 + int save_errno = errno; + struct stat sb; + + if (save_errno != EEXIST || + stat(path, &sb) != 0 || + !S_ISDIR(sb.st_mode)) { - if (last) - errno = EEXIST; - else - errno = ENOTDIR; + /* Don't let stat replace mkdir's errno */ + errno = save_errno; retval = -1; break; } +#else /* WIN32 */ + /* + * On Windows, stat() opens a handle and can transiently fail on a + * directory another process is concurrently creating. Probe with + * a path-based attribute query instead: it requests only + * FILE_READ_ATTRIBUTES and is exempt from share-mode denial, so + * it reliably sees a concurrently-created directory. We assume + * GetFileAttributes() won't change errno. + */ + DWORD attr = GetFileAttributes(path); + + if (errno != EEXIST || + attr == INVALID_FILE_ATTRIBUTES || + !(attr & FILE_ATTRIBUTE_DIRECTORY)) + { + retval = -1; + break; + } +#endif /* WIN32 */ } - else if (mkdir(path, last ? omode : S_IRWXU | S_IRWXG | S_IRWXO) < 0) - { - retval = -1; - break; - } + if (!last) *p = '/'; } From e0252679559adaeee16a7924430f542a1cad3ab7 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Mon, 22 Jun 2026 10:43:01 +0900 Subject: [PATCH 091/250] Strip removed-relation references from PlaceHolderVars at join removal When left-join removal deletes a relation, remove_rel_from_query() updates the relid sets attached to RestrictInfos and EquivalenceMembers, and the canonical PlaceHolderVar held in each PlaceHolderInfo, but it does not rewrite the PlaceHolderVars embedded in clause and EquivalenceClass member expressions. That has been fine, because later processing consults those relid sets rather than the embedded PlaceHolderVars. However, such an expression may afterwards be translated for an appendrel child and have its relids recomputed from scratch by pull_varnos(). If the embedded PlaceHolderVar's phrels still mentions the removed relation, pull_varnos() folds it back in, so the rebuilt clause's relids reference a no-longer-existent relation. That yields a parameterized path keyed on the removed relation, tripping the Assert on root->outer_join_rels in get_eclass_indexes_for_relids(). Fix by stripping the removed relids from the PlaceHolderVars in surviving rels' baserestrictinfo and in EquivalenceClass member expressions, keeping them consistent with the canonical PlaceHolderVars. This is only reachable on v18 and later, where match_index_to_operand() began ignoring PlaceHolderVars; before that, the wrapping PlaceHolderVar prevented the index match that exposes the stale relids. Reported-by: Alexander Kuzmenkov Author: Richard Guo Reviewed-by: Tender Wang Discussion: https://postgr.es/m/CALzhyqwryL2QywgO03VQr_237Sq3MEVgTTT2_A9G3nGT5-SRZg@mail.gmail.com Backpatch-through: 18 --- src/backend/optimizer/plan/analyzejoins.c | 121 ++++++++++++++++++++-- src/test/regress/expected/join.out | 20 ++++ src/test/regress/sql/join.sql | 12 +++ 3 files changed, 143 insertions(+), 10 deletions(-) diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 9f546b4d3a6..5557600988d 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -72,9 +72,11 @@ static void remove_leftjoinrel_from_query(PlannerInfo *root, int relid, SpecialJoinInfo *sjinfo); static void remove_rel_from_restrictinfo(RestrictInfo *rinfo, int relid, int ojrelid); -static void remove_rel_from_eclass(EquivalenceClass *ec, +static void remove_rel_from_eclass(PlannerInfo *root, EquivalenceClass *ec, SpecialJoinInfo *sjinfo, int relid, int subst); +static Node *remove_rel_from_phvs(Node *node, int relid, int ojrelid); +static Node *remove_rel_from_phvs_mutator(Node *node, Relids removable); static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved); static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel); static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, @@ -502,9 +504,7 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, { EquivalenceClass *ec = (EquivalenceClass *) lfirst(l); - if (bms_is_member(relid, ec->ec_relids) || - (sjinfo == NULL || bms_is_member(sjinfo->ojrelid, ec->ec_relids))) - remove_rel_from_eclass(ec, sjinfo, relid, subst); + remove_rel_from_eclass(root, ec, sjinfo, relid, subst); } /* @@ -520,6 +520,11 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, * * So, start by removing all other bits from attr_needed sets and * lateral_vars lists. (We already did this above for ph_needed.) + * + * Also, for left-join removal, we strip the removed rel and join from any + * PlaceHolderVar embedded in the surviving rels' restriction clauses (see + * remove_rel_from_phvs); we needn't bother with the rel being removed, + * nor when the query has no PlaceHolderVars. */ for (rti = 1; rti < root->simple_rel_array_size; rti++) { @@ -545,6 +550,16 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, if (subst > 0) ChangeVarNodesExtended((Node *) otherrel->lateral_vars, relid, subst, 0, replace_relid_callback); + + if (sjinfo != NULL && rti != relid && root->glob->lastPHId != 0) + { + foreach_node(RestrictInfo, rinfo, otherrel->baserestrictinfo) + { + rinfo->clause = (Expr *) + remove_rel_from_phvs((Node *) rinfo->clause, relid, + sjinfo->ojrelid); + } + } } } @@ -741,18 +756,41 @@ remove_rel_from_restrictinfo(RestrictInfo *rinfo, int relid, int ojrelid) * Remove any references to relid or sjinfo->ojrelid (if sjinfo != NULL) * from the EquivalenceClass. * - * Like remove_rel_from_restrictinfo, we don't worry about cleaning out - * any nullingrel bits in contained Vars and PHVs. (This might have to be - * improved sometime.) We do need to fix the EC and EM relid sets to ensure - * that implied join equalities will be generated at the appropriate join - * level(s). + * We fix the EC and EM relid sets to ensure that implied join equalities will + * be generated at the appropriate join level(s). We also strip the removed + * rel from PlaceHolderVars embedded in member expressions; a member's + * em_relids reflects ph_eval_at rather than the PHV's phrels, so the latter + * can still mention the removed rel even when em_relids does not. Like + * remove_rel_from_restrictinfo, we don't bother with nullingrel bits in + * contained plain Vars. */ static void -remove_rel_from_eclass(EquivalenceClass *ec, SpecialJoinInfo *sjinfo, +remove_rel_from_eclass(PlannerInfo *root, EquivalenceClass *ec, + SpecialJoinInfo *sjinfo, int relid, int subst) { ListCell *lc; + /* + * Strip the removed rel/join from PlaceHolderVars in member expressions. + * This is needed even when the EC's relids don't mention the removed rel. + * Plain Vars and Consts can't contain a PlaceHolderVar, so skip them. + */ + if (sjinfo != NULL && root->glob->lastPHId != 0) + { + foreach_node(EquivalenceMember, em, ec->ec_members) + { + if (!IsA(em->em_expr, Var) && !IsA(em->em_expr, Const)) + em->em_expr = (Expr *) + remove_rel_from_phvs((Node *) em->em_expr, relid, + sjinfo->ojrelid); + } + } + + if (!bms_is_member(relid, ec->ec_relids) && + (sjinfo == NULL || !bms_is_member(sjinfo->ojrelid, ec->ec_relids))) + return; + /* Fix up the EC's overall relids */ ec->ec_relids = adjust_relid_set(ec->ec_relids, relid, subst); if (sjinfo != NULL) @@ -809,6 +847,69 @@ remove_rel_from_eclass(EquivalenceClass *ec, SpecialJoinInfo *sjinfo, ec_clear_derived_clauses(ec); } +/* + * Remove any references to the specified RT index(es) from the phrels (and + * phnullingrels) of every PlaceHolderVar in the given expression. + * + * remove_rel_from_query() fixes up the relid sets of RestrictInfos and + * EquivalenceMembers, but not the PlaceHolderVars embedded in their + * expressions. That's normally fine, but such an expression may later be + * translated for an appendrel child and have its relids recomputed by + * pull_varnos(). A leftover removed relid in phrels would then make + * pull_varnos() reference a nonexistent rel, so we strip it here to match the + * canonical PlaceHolderVar. + */ +static Node * +remove_rel_from_phvs(Node *node, int relid, int ojrelid) +{ + Relids removable = bms_add_member(bms_make_singleton(relid), ojrelid); + + return remove_rel_from_phvs_mutator(node, removable); +} + +static Node * +remove_rel_from_phvs_mutator(Node *node, Relids removable) +{ + if (node == NULL) + return NULL; + if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *phv = (PlaceHolderVar *) node; + Relids newphrels; + + /* Upper-level PlaceHolderVars should be long gone at this point */ + Assert(phv->phlevelsup == 0); + + /* Copy the PlaceHolderVar and mutate what's below ... */ + phv = (PlaceHolderVar *) + expression_tree_mutator(node, + remove_rel_from_phvs_mutator, + removable); + + /* + * ... then strip the removed rels from its relid sets. + * + * If stripping would empty phrels, the PHV is evaluated only at the + * removed relation(s); it then belongs to an EquivalenceMember that + * the caller drops immediately afterwards. Leave such a PHV + * untouched rather than build one with empty phrels, which the rest + * of the planner assumes never occurs. + */ + newphrels = bms_difference(phv->phrels, removable); + if (!bms_is_empty(newphrels)) + { + phv->phrels = newphrels; + phv->phnullingrels = bms_difference(phv->phnullingrels, + removable); + } + + return (Node *) phv; + } + return expression_tree_mutator(node, + remove_rel_from_phvs_mutator, + removable); +} + /* * Remove any occurrences of the target relid from a joinlist structure. * diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 02935b0af6d..e9a5ecb1581 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6374,6 +6374,26 @@ select a.* from a left join parted_b pb on a.b_id = pb.id; Seq Scan on a (1 row) +-- test that clauses that still embed PHVs are not referencing the removed +-- relation when rebuilt for a partition of the kept relation +explain (costs off) +select 1 from (select t1.id from parted_b t1 left join parted_b t2 on t1.id = t2.id) s +where s.id = 1 group by (); + QUERY PLAN +------------ + Result +(1 row) + +explain (costs off) +select 1 from parted_b t1 + join (select t2.id from parted_b t2 left join parted_b t3 on t2.id = t3.id) s + on t1.id = s.id +group by (); + QUERY PLAN +------------ + Result +(1 row) + rollback; create temp table parent (k int primary key, pd int); create temp table child (k int unique, cd int); diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 3ac1ac69074..d0f36bec25c 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -2342,6 +2342,18 @@ CREATE TEMP TABLE parted_b1 partition of parted_b for values from (0) to (10); explain (costs off) select a.* from a left join parted_b pb on a.b_id = pb.id; +-- test that clauses that still embed PHVs are not referencing the removed +-- relation when rebuilt for a partition of the kept relation +explain (costs off) +select 1 from (select t1.id from parted_b t1 left join parted_b t2 on t1.id = t2.id) s +where s.id = 1 group by (); + +explain (costs off) +select 1 from parted_b t1 + join (select t2.id from parted_b t2 left join parted_b t3 on t2.id = t3.id) s + on t1.id = s.id +group by (); + rollback; create temp table parent (k int primary key, pd int); From 020426268ff6acaa140780336eb1c50b034cb893 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 22 Jun 2026 12:59:16 -0400 Subject: [PATCH 092/250] pgcrypto: avoid recursive ResourceOwnerForget(). Raising an error within a function using an OSSLCipher object led to a complaint from ResourceOwnerForget and then a double-free crash, because ResOwnerReleaseOSSLCipher forgot to unhook the OSSLCipher object from its owner. (The sibling logic for OSSLDigest objects got this right, as did every other ReleaseResource function AFAICS.) Oversight in cd694f60d. Bug: #19527 Reported-by: Yuelin Wang <3020001251@tju.edu.cn> Author: Yuelin Wang <3020001251@tju.edu.cn> Reviewed-by: Tom Lane Discussion: https://postgr.es/m/19527-6e7686960c6dce78@postgresql.org Backpatch-through: 17 --- contrib/pgcrypto/openssl.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contrib/pgcrypto/openssl.c b/contrib/pgcrypto/openssl.c index f179e80c842..51f28799a41 100644 --- a/contrib/pgcrypto/openssl.c +++ b/contrib/pgcrypto/openssl.c @@ -832,7 +832,10 @@ px_find_cipher(const char *name, PX_Cipher **res) static void ResOwnerReleaseOSSLCipher(Datum res) { - free_openssl_cipher((OSSLCipher *) DatumGetPointer(res)); + OSSLCipher *cipher = (OSSLCipher *) DatumGetPointer(res); + + cipher->owner = NULL; + free_openssl_cipher(cipher); } /* From ac6a58a700da1262d669e970f7ccba66f916c412 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 22 Jun 2026 18:03:23 -0400 Subject: [PATCH 093/250] Fix unsafe order of operations in ResourceOwnerReleaseAll(). This function called the resource-kind-specific ReleaseResource() method for each item before deleting that item from the resowner. That's backwards from the ordering in ResourceOwnerReleaseAllOfKind, and it's not very safe. If ReleaseResource throws an error then the subsequent abort cleanup will come back here and try to release that item again, possibly leading to a double-free or similar crash, and in any case risking an infinite error cleanup loop. This mistake explains why the pgcrypto bug just fixed in 80bb0ebcc led to a crash rather than something more benign. Remove the item from the resowner, then call ReleaseResource, matching the way things were done before b8bff07da. If there is a problem of this sort, we'd prefer to leak the item than suffer the other likely consequences. Per further analysis of bug #19527. Author: Tom Lane Discussion: https://postgr.es/m/646741.1782157515@sss.pgh.pa.us Backpatch-through: 17 --- src/backend/utils/resowner/resowner.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/backend/utils/resowner/resowner.c b/src/backend/utils/resowner/resowner.c index d39f3e1b655..cd2155caabd 100644 --- a/src/backend/utils/resowner/resowner.c +++ b/src/backend/utils/resowner/resowner.c @@ -350,6 +350,7 @@ ResourceOwnerReleaseAll(ResourceOwner owner, ResourceReleasePhase phase, { ResourceElem *items; uint32 nitems; + bool using_arr; /* * ResourceOwnerSort must've been called already. All the resources are @@ -361,12 +362,14 @@ ResourceOwnerReleaseAll(ResourceOwner owner, ResourceReleasePhase phase, { items = owner->arr; nitems = owner->narr; + using_arr = true; } else { Assert(owner->narr == 0); items = owner->hash; nitems = owner->nhash; + using_arr = false; } /* @@ -395,13 +398,20 @@ ResourceOwnerReleaseAll(ResourceOwner owner, ResourceReleasePhase phase, elog(WARNING, "resource was not closed: %s", res_str); pfree(res_str); } - kind->ReleaseResource(value); + + /* + * Update stored count to forget the item before calling its + * ReleaseResource method. This avoids double-free crashes in case an + * error gets thrown within ReleaseResource. + */ nitems--; + if (using_arr) + owner->narr = nitems; + else + owner->nhash = nitems; + + kind->ReleaseResource(value); } - if (owner->nhash == 0) - owner->narr = nitems; - else - owner->nhash = nitems; } From fe464e9e68633c471e8ecb83adb7151decdd437e Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 23 Jun 2026 07:58:04 +0900 Subject: [PATCH 094/250] Re-introduce pgstat_drop_entry(), keeping ABI compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This routine acts as a wrapper of a new pgstat_drop_entry_ext(), used in the core code with a missing_ok argument. This includes an update of .abi-compliance-history, removing the latest entry that has documented the change of pgstat_drop_entry(). This change is applied across v15~v18. HEAD keeps pgstat_drop_entry() as single entry point, with the new missing_ok. Per discussion with Álvaro Herrera and Lukas Fittl. This is a follow-up of 850b9218c8e4. Discussion: https://postgr.es/m/ajZz_sVJVX7pmPHo@alvherre.pgsql Backpatch-through: 15-18 --- .abi-compliance-history | 9 --------- src/backend/utils/activity/pgstat.c | 2 +- src/backend/utils/activity/pgstat_function.c | 4 ++-- src/backend/utils/activity/pgstat_replslot.c | 4 ++-- src/backend/utils/activity/pgstat_shmem.c | 17 ++++++++++++++--- src/backend/utils/activity/pgstat_xact.c | 8 ++++---- src/include/utils/pgstat_internal.h | 5 +++-- .../modules/injection_points/injection_stats.c | 4 ++-- 8 files changed, 28 insertions(+), 25 deletions(-) diff --git a/.abi-compliance-history b/.abi-compliance-history index 6ba3e7519b7..a4bf3336cac 100644 --- a/.abi-compliance-history +++ b/.abi-compliance-history @@ -18,15 +18,6 @@ # Be sure to replace "" with details of your change and # why it is deemed acceptable. -5cc59834b860ed48d710c1baa9c50c66540c64d0 -# -# Fix PANIC with track_functions due to concurrent drop of pgstats entries -# 2026-06-18 11:49:34 +0900 -# -# This commit has added a "missing_ok" argument to pgstat_drop_entry(). All -# the callers of this routine are in core for v15-v17. One custom stats kinds -# available in the public since v18 is impacted (maintainer informed). - 8d9a97e0bb6d820dac553848f0d5d8cc3f3e219d # # Avoid name collision with NOT NULL constraints diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index d997be2e1d9..99e537bc886 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -620,7 +620,7 @@ pgstat_shutdown_hook(int code, Datum arg) dlist_init(&pgStatPending); /* drop the backend stats entry */ - if (!pgstat_drop_entry(PGSTAT_KIND_BACKEND, InvalidOid, MyProcNumber, false)) + if (!pgstat_drop_entry_ext(PGSTAT_KIND_BACKEND, InvalidOid, MyProcNumber, false)) pgstat_request_entry_refs_gc(); pgstat_detach_shmem(); diff --git a/src/backend/utils/activity/pgstat_function.c b/src/backend/utils/activity/pgstat_function.c index f763a0d6f5b..eaeb0dcb1de 100644 --- a/src/backend/utils/activity/pgstat_function.c +++ b/src/backend/utils/activity/pgstat_function.c @@ -112,8 +112,8 @@ pgstat_init_function_usage(FunctionCallInfo fcinfo, AcceptInvalidationMessages(); if (!SearchSysCacheExists1(PROCOID, ObjectIdGetDatum(fcinfo->flinfo->fn_oid))) { - pgstat_drop_entry(PGSTAT_KIND_FUNCTION, MyDatabaseId, - fcinfo->flinfo->fn_oid, true); + pgstat_drop_entry_ext(PGSTAT_KIND_FUNCTION, MyDatabaseId, + fcinfo->flinfo->fn_oid, true); ereport(ERROR, errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("function call to dropped function")); } diff --git a/src/backend/utils/activity/pgstat_replslot.c b/src/backend/utils/activity/pgstat_replslot.c index 746b97a0b5a..675c535266c 100644 --- a/src/backend/utils/activity/pgstat_replslot.c +++ b/src/backend/utils/activity/pgstat_replslot.c @@ -157,8 +157,8 @@ pgstat_drop_replslot(ReplicationSlot *slot) { Assert(LWLockHeldByMeInMode(ReplicationSlotAllocationLock, LW_EXCLUSIVE)); - if (!pgstat_drop_entry(PGSTAT_KIND_REPLSLOT, InvalidOid, - ReplicationSlotIndex(slot), false)) + if (!pgstat_drop_entry_ext(PGSTAT_KIND_REPLSLOT, InvalidOid, + ReplicationSlotIndex(slot), false)) pgstat_request_entry_refs_gc(); } diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c index 414b66be2dc..056c24c6ee1 100644 --- a/src/backend/utils/activity/pgstat_shmem.c +++ b/src/backend/utils/activity/pgstat_shmem.c @@ -880,7 +880,7 @@ pgstat_free_entry(PgStatShared_HashEntry *shent, dshash_seq_status *hstat) /* * Helper for both pgstat_drop_database_and_contents() and - * pgstat_drop_entry(). If hstat is non-null delete the shared entry using + * pgstat_drop_entry_ext(). If hstat is non-null delete the shared entry using * dshash_delete_current(), otherwise use dshash_delete_entry(). In either * case the entry needs to be already locked. */ @@ -968,6 +968,17 @@ pgstat_drop_database_and_contents(Oid dboid) pgstat_request_entry_refs_gc(); } +/* + * ABI-preserving wrapper around pgstat_drop_entry_ext(). + * + * The original routine introduced in v15 did not include "missing_ok". + */ +bool +pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid) +{ + return pgstat_drop_entry_ext(kind, dboid, objid, false); +} + /* * Drop a single stats entry. * @@ -982,8 +993,8 @@ pgstat_drop_database_and_contents(Oid dboid) * pgstat_gc_entry_refs(). */ bool -pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid, - bool missing_ok) +pgstat_drop_entry_ext(PgStat_Kind kind, Oid dboid, uint64 objid, + bool missing_ok) { PgStat_HashKey key; PgStatShared_HashEntry *shent; diff --git a/src/backend/utils/activity/pgstat_xact.c b/src/backend/utils/activity/pgstat_xact.c index b058c978dcf..fbbf2844a58 100644 --- a/src/backend/utils/activity/pgstat_xact.c +++ b/src/backend/utils/activity/pgstat_xact.c @@ -85,7 +85,7 @@ AtEOXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, bool isCommit) * Transaction that dropped an object committed. Drop the stats * too. */ - if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) + if (!pgstat_drop_entry_ext(it->kind, it->dboid, objid, true)) not_freed_count++; } else if (!isCommit && pending->is_create) @@ -94,7 +94,7 @@ AtEOXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, bool isCommit) * Transaction that created an object aborted. Drop the stats * associated with the object. */ - if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) + if (!pgstat_drop_entry_ext(it->kind, it->dboid, objid, true)) not_freed_count++; } @@ -160,7 +160,7 @@ AtEOSubXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, * Subtransaction creating a new stats object aborted. Drop the * stats object. */ - if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) + if (!pgstat_drop_entry_ext(it->kind, it->dboid, objid, true)) not_freed_count++; pfree(pending); } @@ -323,7 +323,7 @@ pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, xl_xact_stats_item *it = &items[i]; uint64 objid = ((uint64) it->objid_hi) << 32 | it->objid_lo; - if (!pgstat_drop_entry(it->kind, it->dboid, objid, true)) + if (!pgstat_drop_entry_ext(it->kind, it->dboid, objid, true)) not_freed_count++; } diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index 4f840c38f07..c09eda3a84d 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -708,8 +708,9 @@ extern PgStat_EntryRef *pgstat_get_entry_ref(PgStat_Kind kind, Oid dboid, uint64 extern bool pgstat_lock_entry(PgStat_EntryRef *entry_ref, bool nowait); extern bool pgstat_lock_entry_shared(PgStat_EntryRef *entry_ref, bool nowait); extern void pgstat_unlock_entry(PgStat_EntryRef *entry_ref); -extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid, - bool missing_ok); +extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid); +extern bool pgstat_drop_entry_ext(PgStat_Kind kind, Oid dboid, uint64 objid, + bool missing_ok); extern void pgstat_drop_all_entries(void); extern void pgstat_drop_matching_entries(bool (*do_drop) (PgStatShared_HashEntry *, Datum), Datum match_data); diff --git a/src/test/modules/injection_points/injection_stats.c b/src/test/modules/injection_points/injection_stats.c index 435a0484a4b..c659b2660d4 100644 --- a/src/test/modules/injection_points/injection_stats.c +++ b/src/test/modules/injection_points/injection_stats.c @@ -149,8 +149,8 @@ pgstat_drop_inj(const char *name) if (!inj_stats_loaded || !inj_stats_enabled) return; - if (!pgstat_drop_entry(PGSTAT_KIND_INJECTION, InvalidOid, - PGSTAT_INJ_IDX(name), false)) + if (!pgstat_drop_entry_ext(PGSTAT_KIND_INJECTION, InvalidOid, + PGSTAT_INJ_IDX(name), false)) pgstat_request_entry_refs_gc(); } From d3ff08e66b430215e1999c07e6f0c6573e6d39d9 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 23 Jun 2026 16:52:15 +0900 Subject: [PATCH 095/250] doc: Describe better handling of indexes in ALTER TABLE ATTACH PARTITION When ALTER TABLE ... ATTACH PARTITION matches partition indexes to the parent table's indexes, invalid indexes are skipped. This commit improves the documentation to describe what e90e9275f56 has changed: invalid indexes are skipped, and only valid indexes are considered for a match. Author: Mohamed Ali Reviewed-by: Sami Imseih Discussion: https://postgr.es/m/CAGnOmWpAMaE-BOkpwM6mJnHcpS2QZ8yLSSaqmz+vryEsbCWWWA@mail.gmail.com Backpatch-through: 14 --- doc/src/sgml/ref/alter_table.sgml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 409164bf180..31f2d91418b 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -1037,10 +1037,11 @@ WITH ( MODULUS numeric_literal, REM as a partition of the target table. The table can be attached as a partition for specific values using FOR VALUES or as a default partition by using DEFAULT. - For each index in the target table, a corresponding - one will be created in the attached table; or, if an equivalent - index already exists, it will be attached to the target table's index, - as if ALTER INDEX ATTACH PARTITION had been executed. + For each index in the target table, if a valid equivalent index + already exists in the partition, it will be attached to the target + table's index, as if ALTER INDEX ATTACH PARTITION had been executed; + otherwise, a new corresponding index will be created. Invalid indexes + on the partition are skipped. Note that if the existing table is a foreign table, it is currently not allowed to attach the table as a partition of the target table if there are UNIQUE indexes on the target table. (See also From 1ef917e3a61a26a264fe65012d951ac36aa03732 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Mon, 22 Jun 2026 17:21:45 +0900 Subject: [PATCH 096/250] Re-index ModifyTable FDW arrays when pruning result relations ExecInitModifyTable() rebuilds the per-result-relation lists after dropping result relations removed by initial runtime pruning. The re-indexing was done for withCheckOptionLists, returningLists, updateColnosLists, mergeActionLists and mergeJoinConditions, but fdwPrivLists and fdwDirectModifyPlans were missed. As a result, a kept foreign result relation could be handed the wrong fdw_private, or ri_usesFdwDirectModify could be set from the wrong plan index, leading to wrong behavior or a crash in BeginForeignModify() and in the direct-modify path. show_modifytable_info() had the same problem: it indexed the plan-ordered node->fdwPrivLists with the post-pruning executor position, so once initial pruning removed a result relation it could read a different relation's fdw_private (often a NIL entry), producing wrong EXPLAIN output or a crash. Fix by re-indexing fdwPrivLists and fdwDirectModifyPlans alongside the other lists, saving the re-indexed private lists in ModifyTableState.mt_fdwPrivLists and reading from there in both nodeModifyTable.c and explain.c. Reported-by: Chi Zhang <798604270@qq.com> Author: Ayush Tiwari Author: Rafia Sabih Reviewed-by: Matheus Alcantara Reviewed-by: Etsuro Fujita Discussion: https://postgr.es/m/19484-a3cb82c8cde3c8fa%40postgresql.org Backpatch-through: 18 --- .../postgres_fdw/expected/postgres_fdw.out | 64 +++++++++++++++++++ contrib/postgres_fdw/sql/postgres_fdw.sql | 34 ++++++++++ src/backend/commands/explain.c | 2 +- src/backend/executor/nodeModifyTable.c | 22 ++++++- src/include/nodes/execnodes.h | 8 ++- 5 files changed, 124 insertions(+), 6 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index f975ceff200..3cc3c6dc3c7 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -7132,6 +7132,70 @@ RESET enable_material; DROP FOREIGN TABLE remt2; DROP TABLE loct1; DROP TABLE loct2; +-- Test that direct modify and foreign modify work with runtime pruning of +-- result relations (bug #19484) +create table fdw_part_update (a int not null, b int) partition by list (a); +create table fdw_part_update_p1 partition of fdw_part_update for values in (1); +create table fdw_part_update_remote (a int not null, b int); +create foreign table fdw_part_update_p2 partition of fdw_part_update + for values in (2) + server loopback options (table_name 'fdw_part_update_remote'); +insert into fdw_part_update_p1 values (1, 10); +insert into fdw_part_update_remote values (2, 20); +set plan_cache_mode = force_generic_plan; +-- Check DirectModify case +prepare fdw_part_upd(int) as + update fdw_part_update set b = b + 1 where a = $1 + returning tableoid::regclass, a, b; +explain (verbose, costs off) + execute fdw_part_upd(2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Update on public.fdw_part_update + Output: (fdw_part_update_1.tableoid)::regclass, fdw_part_update_1.a, fdw_part_update_1.b + Foreign Update on public.fdw_part_update_p2 fdw_part_update_2 + -> Append + Subplans Removed: 1 + -> Foreign Update on public.fdw_part_update_p2 fdw_part_update_2 + Remote SQL: UPDATE public.fdw_part_update_remote SET b = (b + 1) WHERE ((a = $1::integer)) RETURNING a, b +(7 rows) + +execute fdw_part_upd(2); + tableoid | a | b +--------------------+---+---- + fdw_part_update_p2 | 2 | 21 +(1 row) + +deallocate fdw_part_upd; +-- Check ForeignModify case +prepare fdw_part_upd2(int) as + update fdw_part_update set b = b + random()::int * 0 + 1 where a = $1 + returning tableoid::regclass, a, b; +explain (verbose, costs off) + execute fdw_part_upd2(2); + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------- + Update on public.fdw_part_update + Output: (fdw_part_update_1.tableoid)::regclass, fdw_part_update_1.a, fdw_part_update_1.b + Foreign Update on public.fdw_part_update_p2 fdw_part_update_2 + Remote SQL: UPDATE public.fdw_part_update_remote SET b = $2 WHERE ctid = $1 RETURNING a, b + -> Append + Subplans Removed: 1 + -> Foreign Scan on public.fdw_part_update_p2 fdw_part_update_2 + Output: ((fdw_part_update_2.b + ((random())::integer * 0)) + 1), fdw_part_update_2.tableoid, fdw_part_update_2.ctid, fdw_part_update_2.* + Remote SQL: SELECT a, b, ctid FROM public.fdw_part_update_remote WHERE ((a = $1::integer)) FOR UPDATE +(9 rows) + +execute fdw_part_upd2(2); + tableoid | a | b +--------------------+---+---- + fdw_part_update_p2 | 2 | 22 +(1 row) + +deallocate fdw_part_upd2; +reset plan_cache_mode; +drop table fdw_part_update; +drop table fdw_part_update_remote; -- =================================================================== -- test check constraints -- =================================================================== diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 04a5a1244e8..22e24bb024f 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -1732,6 +1732,40 @@ DROP FOREIGN TABLE remt2; DROP TABLE loct1; DROP TABLE loct2; +-- Test that direct modify and foreign modify work with runtime pruning of +-- result relations (bug #19484) +create table fdw_part_update (a int not null, b int) partition by list (a); +create table fdw_part_update_p1 partition of fdw_part_update for values in (1); +create table fdw_part_update_remote (a int not null, b int); +create foreign table fdw_part_update_p2 partition of fdw_part_update + for values in (2) + server loopback options (table_name 'fdw_part_update_remote'); +insert into fdw_part_update_p1 values (1, 10); +insert into fdw_part_update_remote values (2, 20); +set plan_cache_mode = force_generic_plan; + +-- Check DirectModify case +prepare fdw_part_upd(int) as + update fdw_part_update set b = b + 1 where a = $1 + returning tableoid::regclass, a, b; +explain (verbose, costs off) + execute fdw_part_upd(2); +execute fdw_part_upd(2); +deallocate fdw_part_upd; + +-- Check ForeignModify case +prepare fdw_part_upd2(int) as + update fdw_part_update set b = b + random()::int * 0 + 1 where a = $1 + returning tableoid::regclass, a, b; +explain (verbose, costs off) + execute fdw_part_upd2(2); +execute fdw_part_upd2(2); +deallocate fdw_part_upd2; + +reset plan_cache_mode; +drop table fdw_part_update; +drop table fdw_part_update_remote; + -- =================================================================== -- test check constraints -- =================================================================== diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 7e2792ead71..6d2624e75b8 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -4609,7 +4609,7 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, fdwroutine != NULL && fdwroutine->ExplainForeignModify != NULL) { - List *fdw_private = (List *) list_nth(node->fdwPrivLists, j); + List *fdw_private = (List *) list_nth(mtstate->mt_fdwPrivLists, j); fdwroutine->ExplainForeignModify(mtstate, resultRelInfo, diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index ea8ed94a764..cac30666663 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -4647,6 +4647,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) List *updateColnosLists = NIL; List *mergeActionLists = NIL; List *mergeJoinConditions = NIL; + List *fdwPrivLists = NIL; + Bitmapset *fdwDirectModifyPlans = NULL; ResultRelInfo *resultRelInfo; List *arowmarks; ListCell *l; @@ -4689,6 +4691,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) if (keep_rel) { + List *fdwPrivList = (List *) list_nth(node->fdwPrivLists, i); + resultRelations = lappend_int(resultRelations, rti); if (node->withCheckOptionLists) { @@ -4724,6 +4728,19 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mergeJoinConditions = lappend(mergeJoinConditions, mergeJoinCondition); } + + /* + * fdwPrivLists/fdwDirectModifyPlans are re-indexed to match + * resultRelations + */ + fdwPrivLists = lappend(fdwPrivLists, fdwPrivList); + if (bms_is_member(i, node->fdwDirectModifyPlans)) + { + int new_index = list_length(resultRelations) - 1; + + fdwDirectModifyPlans = bms_add_member(fdwDirectModifyPlans, + new_index); + } } i++; } @@ -4753,6 +4770,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->mt_updateColnosLists = updateColnosLists; mtstate->mt_mergeActionLists = mergeActionLists; mtstate->mt_mergeJoinConditions = mergeJoinConditions; + mtstate->mt_fdwPrivLists = fdwPrivLists; /*---------- * Resolve the target relation. This is the same as: @@ -4828,7 +4846,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) /* Initialize the usesFdwDirectModify flag */ resultRelInfo->ri_usesFdwDirectModify = - bms_is_member(i, node->fdwDirectModifyPlans); + bms_is_member(i, fdwDirectModifyPlans); /* * Verify result relation is a valid target for the current operation @@ -4857,7 +4875,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) resultRelInfo->ri_FdwRoutine != NULL && resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL) { - List *fdw_private = (List *) list_nth(node->fdwPrivLists, i); + List *fdw_private = (List *) list_nth(fdwPrivLists, i); resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate, resultRelInfo, diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 409e172bfb6..6677a03caab 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1453,13 +1453,15 @@ typedef struct ModifyTableState double mt_merge_deleted; /* - * Lists of valid updateColnosLists, mergeActionLists, and - * mergeJoinConditions. These contain only entries for unpruned - * relations, filtered from the corresponding lists in ModifyTable. + * Lists of valid updateColnosLists, mergeActionLists, + * mergeJoinConditions, and fdwPrivLists. These contain only entries for + * unpruned relations, filtered from the corresponding lists in + * ModifyTable. */ List *mt_updateColnosLists; List *mt_mergeActionLists; List *mt_mergeJoinConditions; + List *mt_fdwPrivLists; } ModifyTableState; /* ---------------- From e430ecc5958bb1ef95133f5b3c83dbb98ac255c8 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Wed, 24 Jun 2026 09:09:48 +0900 Subject: [PATCH 097/250] plperl: Fix NULL pointer dereference for forged array object In get_perl_array_ref(), for a PostgreSQL::InServer::ARRAY object, we look up its "array" key with hv_fetch_string() and then inspect the returned SV. However, hv_fetch_string() returns a NULL pointer when the key is absent, and the code dereferenced that result without first checking whether the pointer itself was NULL. As a result, a plperl function returning a forged PostgreSQL::InServer::ARRAY object that lacks the "array" key would crash the backend with a segmentation fault. Fix this by checking the pointer returned by hv_fetch_string() before dereferencing it, matching how other callers in this file already guard the result. With the check in place, such an object falls through to the existing error report instead of crashing. Author: Xing Guo Reviewed-by: Richard Guo Discussion: https://postgr.es/m/CACpMh+DYgcnqZwQLXXuxQcehJTd7T8UmKWSLsK4mFBEp9G2ajA@mail.gmail.com Backpatch-through: 14 --- src/pl/plperl/expected/plperl_array.out | 7 +++++++ src/pl/plperl/plperl.c | 2 +- src/pl/plperl/sql/plperl_array.sql | 7 +++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pl/plperl/expected/plperl_array.out b/src/pl/plperl/expected/plperl_array.out index 260a55ea7e9..f5803e10a6e 100644 --- a/src/pl/plperl/expected/plperl_array.out +++ b/src/pl/plperl/expected/plperl_array.out @@ -274,3 +274,10 @@ select perl_setof_array('{{1}, {2}, {3}}'); {3} (3 rows) +-- Test a forged PostgreSQL::InServer::ARRAY object lacking the 'array' key +CREATE OR REPLACE FUNCTION perl_forged_array() RETURNS integer[] AS $$ + return bless {}, "PostgreSQL::InServer::ARRAY"; +$$ LANGUAGE plperl; +SELECT perl_forged_array(); +ERROR: could not get array reference from PostgreSQL::InServer::ARRAY object +CONTEXT: PL/Perl function "perl_forged_array" diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c index 29cb4d7e47f..1c52084403b 100644 --- a/src/pl/plperl/plperl.c +++ b/src/pl/plperl/plperl.c @@ -1150,7 +1150,7 @@ get_perl_array_ref(SV *sv) HV *hv = (HV *) SvRV(sv); SV **sav = hv_fetch_string(hv, "array"); - if (*sav && SvOK(*sav) && SvROK(*sav) && + if (sav && *sav && SvOK(*sav) && SvROK(*sav) && SvTYPE(SvRV(*sav)) == SVt_PVAV) return *sav; diff --git a/src/pl/plperl/sql/plperl_array.sql b/src/pl/plperl/sql/plperl_array.sql index ca63b5db625..cd1d7e34c50 100644 --- a/src/pl/plperl/sql/plperl_array.sql +++ b/src/pl/plperl/sql/plperl_array.sql @@ -206,3 +206,10 @@ create or replace function perl_setof_array(integer[]) returns setof integer[] l $$; select perl_setof_array('{{1}, {2}, {3}}'); + +-- Test a forged PostgreSQL::InServer::ARRAY object lacking the 'array' key +CREATE OR REPLACE FUNCTION perl_forged_array() RETURNS integer[] AS $$ + return bless {}, "PostgreSQL::InServer::ARRAY"; +$$ LANGUAGE plperl; + +SELECT perl_forged_array(); From bba4e095d25005dd0b2f180187ff2c2adf48a3b3 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Thu, 25 Jun 2026 12:12:59 +0900 Subject: [PATCH 098/250] Avoid ABI break in ModifyTableState from the FDW pruning fix Commit 1ef917e3a6 fixed the re-indexing of ModifyTable's FDW arrays when initial runtime pruning removes result relations, but it did so by adding a new mt_fdwPrivLists field to ModifyTableState. Although the field was placed at the end of the struct to keep the offsets of existing fields stable, it still enlarges sizeof(ModifyTableState), which the ABI compliance check flags on the buildfarm (e.g. crake). The field existed only so that show_modifytable_info() could recover the re-indexed fdw_private after executor startup; the executor-side fix in ExecInitModifyTable() that actually prevents the crash does not depend on it. Remove the field and have show_modifytable_info() instead look up each kept relation's fdw_private from the original, pre-pruning node->fdwPrivLists, which is parallel to node->resultRelations and left intact by pruning. When nothing was pruned the lookup is a direct index; otherwise it matches on the range table index. This is applied to REL_18 only; master keeps the mt_fdwPrivLists field and is unaffected, so the two diverge slightly here. Reported on the buildfarm (member crake). Per a suggestion from Tom Lane. Reviewed-by: Etsuro Fujita Discussion: https://postgr.es/m/CA+HiwqEhe7-v5Q0-oOoW3RaO4voYcGK-JfinbYEWXwutDGSOtQ@mail.gmail.com --- src/backend/commands/explain.c | 36 +++++++++++++++++++++++++- src/backend/executor/nodeModifyTable.c | 1 - src/include/nodes/execnodes.h | 8 +++--- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 6d2624e75b8..15ef3304863 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -4524,6 +4524,7 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, const char *operation; const char *foperation; bool labeltargets; + bool nopruning; int j; List *idxNames = NIL; ListCell *lst; @@ -4571,6 +4572,16 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, if (labeltargets) ExplainOpenGroup("Target Tables", "Target Tables", false, es); + /* + * node->fdwPrivLists is parallel to node->resultRelations, in the + * original pre-pruning order. If no result relations were pruned, the + * entries in mtstate->resultRelInfo[] are in that same order and can be + * matched to fdwPrivLists positionally; otherwise we have to look each + * one up by range table index below. This test is loop-invariant, so + * compute it once here. + */ + nopruning = (list_length(node->resultRelations) == mtstate->mt_nrels); + for (j = 0; j < mtstate->mt_nrels; j++) { ResultRelInfo *resultRelInfo = mtstate->resultRelInfo + j; @@ -4609,7 +4620,30 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, fdwroutine != NULL && fdwroutine->ExplainForeignModify != NULL) { - List *fdw_private = (List *) list_nth(mtstate->mt_fdwPrivLists, j); + List *fdw_private; + + /* + * Find this relation's fdw_private: index fdwPrivLists directly + * when nothing was pruned, else match by range table index. + */ + if (nopruning) + fdw_private = (List *) list_nth(node->fdwPrivLists, j); + else + { + Index rti = resultRelInfo->ri_RangeTableIndex; + ListCell *lc1; + ListCell *lc2; + + fdw_private = NIL; + forboth(lc1, node->resultRelations, lc2, node->fdwPrivLists) + { + if (lfirst_int(lc1) == (int) rti) + { + fdw_private = (List *) lfirst(lc2); + break; + } + } + } fdwroutine->ExplainForeignModify(mtstate, resultRelInfo, diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index cac30666663..7c1d0e9588e 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -4770,7 +4770,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->mt_updateColnosLists = updateColnosLists; mtstate->mt_mergeActionLists = mergeActionLists; mtstate->mt_mergeJoinConditions = mergeJoinConditions; - mtstate->mt_fdwPrivLists = fdwPrivLists; /*---------- * Resolve the target relation. This is the same as: diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 6677a03caab..409e172bfb6 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1453,15 +1453,13 @@ typedef struct ModifyTableState double mt_merge_deleted; /* - * Lists of valid updateColnosLists, mergeActionLists, - * mergeJoinConditions, and fdwPrivLists. These contain only entries for - * unpruned relations, filtered from the corresponding lists in - * ModifyTable. + * Lists of valid updateColnosLists, mergeActionLists, and + * mergeJoinConditions. These contain only entries for unpruned + * relations, filtered from the corresponding lists in ModifyTable. */ List *mt_updateColnosLists; List *mt_mergeActionLists; List *mt_mergeJoinConditions; - List *mt_fdwPrivLists; } ModifyTableState; /* ---------------- From 917fdbc633e24e652427e102bf51484f3e1ab2f2 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 25 Jun 2026 16:58:29 -0400 Subject: [PATCH 099/250] Fix null-pointer crash in ECPG compiler. When compiling a DECLARE section containing a union nested inside a struct, ecpg passes a null value for struct_sizeof to ECPGmake_struct_type. I (tgl) didn't foresee that case in commit 0e6060790, and wrote an unprotected mm_strdup() call. Reported-by: iMSA (via Jehan-Guillaume de Rorthais ) Author: Jehan-Guillaume de Rorthais Reviewed-by: Tom Lane Discussion: https://postgr.es/m/20260625114849.34b2148e@karst Backpatch-through: 18 --- src/interfaces/ecpg/preproc/type.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/ecpg/preproc/type.c b/src/interfaces/ecpg/preproc/type.c index 9f6dacd2aea..39124c93919 100644 --- a/src/interfaces/ecpg/preproc/type.c +++ b/src/interfaces/ecpg/preproc/type.c @@ -101,7 +101,7 @@ ECPGmake_struct_type(struct ECPGstruct_member *rm, enum ECPGttype type, ne->type_name = mm_strdup(type_name); ne->u.members = ECPGstruct_member_dup(rm); - ne->struct_sizeof = mm_strdup(struct_sizeof); + ne->struct_sizeof = struct_sizeof ? mm_strdup(struct_sizeof) : NULL; return ne; } From 3bf2cb22576eac50d80e3eecd485fcae4fdd7f8b Mon Sep 17 00:00:00 2001 From: Tomas Vondra Date: Fri, 26 Jun 2026 19:34:14 +0200 Subject: [PATCH 100/250] Fix out-of-bounds access in autoprewarm worker The read stream callback apw_read_stream_next_block() advances p->pos through the block_info array. When processing the last block, it increments p->pos to prewarm_stop_idx before returning. The callback itself is safe because it checks bounds before accessing the array. However, the caller assigned blk from block_info[i] at the end of the loop body, before the loop condition was re-evaluated. When i equaled prewarm_stop_idx, this accessed memory beyond the allocated DSM segment, causing a segfault. Restructure the loop to check bounds at the top and assign blk at the beginning of the loop body, where it is always safe. This avoids the need for an explicit bounds check at the end. Backpatch to 18, where the bug was introduced by commit 6acab8bdbcda. Author: Matheus Alcantara Reported-by: Glauber Batista Reviewed-by: Melanie Plageman Reviewed-by: Tomas Vondra Backpatch-through: 18 Discussion: https://www.postgresql.org/message-id/CAO%2B_mTQgQyTYwDh%3DU8iTnsDmOGyWsZJjUV31SmEYwmw6_xY6Bw%40mail.gmail.com --- contrib/pg_prewarm/autoprewarm.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c index c01b9c7e6a4..b5ac0e9ceb8 100644 --- a/contrib/pg_prewarm/autoprewarm.c +++ b/contrib/pg_prewarm/autoprewarm.c @@ -573,16 +573,23 @@ autoprewarm_database_main(Datum main_arg) * move on. */ while (i < apw_state->prewarm_stop_idx && - blk.tablespace == tablespace && - blk.filenumber == filenumber && have_free_buffer()) { - ForkNumber forknum = blk.forknum; + ForkNumber forknum; BlockNumber nblocks; struct AutoPrewarmReadStreamData p; ReadStream *stream; Buffer buf; + blk = block_info[i]; + + /* Stop when we reach a different relation. */ + if (blk.tablespace != tablespace || + blk.filenumber != filenumber) + break; + + forknum = blk.forknum; + /* * smgrexists is not safe for illegal forknum, hence check whether * the passed forknum is valid before using it in smgrexists. @@ -644,9 +651,12 @@ autoprewarm_database_main(Datum main_arg) read_stream_end(stream); - /* Advance i past all the blocks just prewarmed. */ + /* + * Advance i past all the blocks just prewarmed. Note that the + * callback might have advanced the index beyond the last valid + * block, so don't access block_info[i] yet. + */ i = p.pos; - blk = block_info[i]; } relation_close(rel, AccessShareLock); From 5fd1c3f287189cfa4bff4ac3492c313417dff7c9 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 28 Jun 2026 12:31:29 -0400 Subject: [PATCH 101/250] 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 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index c9ff71844e7..9a6effe206f 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -4956,6 +4956,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) From 53482fcb94a82cc22e20a7206c508a39aa07a76d Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Mon, 29 Jun 2026 11:38:39 +0900 Subject: [PATCH 102/250] 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 5fb56a2f65d..5f8315e84dd 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 b0af13945bb..6877cc63ce7 100644 --- a/contrib/hstore_plpython/hstore_plpython.c +++ b/contrib/hstore_plpython/hstore_plpython.c @@ -142,7 +142,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(); { @@ -159,6 +168,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 ebd61e6c467..a2b2046380f 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 cac963de69c..8d3f5328809 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 ef5a3d3ead3..657e686ba8f 100644 --- a/contrib/jsonb_plpython/jsonb_plpython.c +++ b/contrib/jsonb_plpython/jsonb_plpython.c @@ -273,7 +273,12 @@ PLyMapping_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state) JsonbValue *volatile out; 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(); { @@ -285,8 +290,15 @@ PLyMapping_ToJsonbValue(PyObject *obj, JsonbParseState **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) @@ -338,7 +350,10 @@ PLySequence_ToJsonbValue(PyObject *obj, JsonbParseState **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); (void) 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 29dc33279a0..fd8485c89c1 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 674af93ddcf..ffce7fc1be7 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 b572f9bf73b..0320ff01f6b 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 8a680e15c14..0cb3d6ea8c6 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 cc74c4df6ba..0725fbc19f2 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 1e386aadcca..ce9f57060cf 100644 --- a/src/pl/plpython/plpy_spi.c +++ b/src/pl/plpython/plpy_spi.c @@ -86,6 +86,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 @@ -248,6 +253,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 f6509a41902..462778e8afe 100644 --- a/src/pl/plpython/plpy_typeio.c +++ b/src/pl/plpython/plpy_typeio.c @@ -1209,6 +1209,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(); { @@ -1455,7 +1459,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 1bb9b83b719..b401b3f2f6b 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 00dcc8bb669..276d130431e 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 0985a9cca2f..31549c7f4f1 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 d36b728949bf4e37ada1cd23e0f2aaa94f609a70 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 29 Jun 2026 11:49:11 +0200 Subject: [PATCH 103/250] 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 38d8e8a2dc9..682028fb59e 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 627605713074e51f29b623cd799e37a9f411c57b Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 30 Jun 2026 08:50:50 +0900 Subject: [PATCH 104/250] 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.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 8d97067fe54..d4cb6246b48 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 = (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 db5a314edf8..0d67f256afe 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.c b/src/backend/commands/sequence.c index a79ef0651a9..c1ad656397a 100644 --- a/src/backend/commands/sequence.c +++ b/src/backend/commands/sequence.c @@ -1933,6 +1933,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 a1870d8e5aa..7639bd523e1 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 5245fdde43c..38e1e43e041 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -58,6 +58,7 @@ tests += { 't/047_checkpoint_physical_slot.pl', 't/048_vacuum_horizon_floor.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 00000000000..96d1e4bf18b --- /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 6a6cf80e5508c8de3e46b257e18117373ebbd289 Mon Sep 17 00:00:00 2001 From: John Naylor Date: Wed, 1 Jul 2026 08:50:08 +0700 Subject: [PATCH 105/250] Document wal_compression=on Commit 4035cd5d4 added LZ4 compression for full-page writes in WAL, and retained "on" as a backward-compatible way to specify the builtin PGLZ method. Document this meaning of "on" and update postgresql.conf.sample to make the equivalence clear. Author: Christoph Berg Reviewed-by: Michael Paquier Discussion: https://postgr.es/m/akJDHRtXwGLTppsQ@msg.df7cb.de Backpatch-through: 15 --- doc/src/sgml/config.sgml | 1 + src/backend/utils/misc/postgresql.conf.sample | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 607dafcb2ed..c688ed05f72 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3457,6 +3457,7 @@ include_dir 'conf.d' was compiled with ) and zstd (if PostgreSQL was compiled with ). + The value on is a historical spelling of pglz. The default value is off. Only superusers and users with the appropriate SET privilege can change this setting. diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index d91133dbd73..0857667d0d6 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -250,7 +250,7 @@ #wal_log_hints = off # also do full page writes of non-critical updates # (change requires restart) #wal_compression = off # enables compression of full-page writes; - # off, pglz, lz4, zstd, or on + # off, pglz (or "on"), lz4, or zstd #wal_init_zero = on # zero-fill new WAL files #wal_recycle = on # recycle WAL files #wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers From 215ab56119e02ec77b3bd417ccf5334f9259bb52 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 1 Jul 2026 09:40:36 +0200 Subject: [PATCH 106/250] Don't cast off_t to 32-bit type for output, bug fix off_t is most likely a 64-bit integer, so casting it to a 32-bit type for output could lose data. There are more issues like this in the tree, but this is an instance where this could actually happen in practice, since base backups are routinely larger than 4 GB. So this is separated out as a bug fix. Reviewed-by: Heikki Linnakangas Discussion: https://www.postgresql.org/message-id/flat/20ce62fa-47fc-457b-b504-12f3c1651726%40eisentraut.org --- src/backend/backup/basebackup_server.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/backup/basebackup_server.c b/src/backend/backup/basebackup_server.c index f5c0c61640a..66ce8e68d26 100644 --- a/src/backend/backup/basebackup_server.c +++ b/src/backend/backup/basebackup_server.c @@ -176,9 +176,9 @@ bbsink_server_archive_contents(bbsink *sink, size_t len) /* short write: complain appropriately */ ereport(ERROR, (errcode(ERRCODE_DISK_FULL), - errmsg("could not write file \"%s\": wrote only %d of %d bytes at offset %u", + errmsg("could not write file \"%s\": wrote only %d of %d bytes at offset %lld", FilePathName(mysink->file), - nbytes, (int) len, (unsigned) mysink->filepos), + nbytes, (int) len, (long long) mysink->filepos), errhint("Check free disk space."))); } @@ -269,9 +269,9 @@ bbsink_server_manifest_contents(bbsink *sink, size_t len) /* short write: complain appropriately */ ereport(ERROR, (errcode(ERRCODE_DISK_FULL), - errmsg("could not write file \"%s\": wrote only %d of %d bytes at offset %u", + errmsg("could not write file \"%s\": wrote only %d of %d bytes at offset %lld", FilePathName(mysink->file), - nbytes, (int) len, (unsigned) mysink->filepos), + nbytes, (int) len, (long long) mysink->filepos), errhint("Check free disk space."))); } From e7564ee8cdcbdc7e8ed06b31bbcd0f80d6e81cd3 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 1 Jul 2026 23:03:08 +0900 Subject: [PATCH 107/250] Clear base backup progress on backup failure Previously, if a base backup failed after it had started streaming files, pg_stat_progress_basebackup could continue to show a stale progress entry even though the backup was no longer running. This could be observed when the client kept the replication connection open after the error. It is normally not observable when using pg_basebackup, because the client disconnects after the error. The problem was that progress reporting was cleared only after successful completion. This commit moves the progress reporting cleanup into the progress sink's cleanup callback so that it is cleared after both successful and failed backups. Backpatch to v15. v14 has the same issue, but the fix does not apply cleanly because it lacks the base backup sink infrastructure. Since the bug does not affect the backup itself and is normally not observable when using pg_basebackup, skip the v14 backpatch. Author: Chao Li Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/EA1A6CD2-EFA6-462B-9A02-03003555AB4A@gmail.com Backpatch-through: 15 --- src/backend/backup/basebackup.c | 2 -- src/backend/backup/basebackup_progress.c | 22 ++++++++++++---------- src/include/backup/basebackup_sink.h | 1 - 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c index f0f88838dc2..bde795aaef0 100644 --- a/src/backend/backup/basebackup.c +++ b/src/backend/backup/basebackup.c @@ -674,8 +674,6 @@ perform_base_backup(basebackup_options *opt, bbsink *sink, /* clean up the resource owner we created */ ReleaseAuxProcessResources(true); - - basebackup_progress_done(); } /* diff --git a/src/backend/backup/basebackup_progress.c b/src/backend/backup/basebackup_progress.c index 1d22b541f89..e829d0626a4 100644 --- a/src/backend/backup/basebackup_progress.c +++ b/src/backend/backup/basebackup_progress.c @@ -38,6 +38,7 @@ static void bbsink_progress_begin_backup(bbsink *sink); static void bbsink_progress_archive_contents(bbsink *sink, size_t len); static void bbsink_progress_end_archive(bbsink *sink); +static void bbsink_progress_cleanup(bbsink *sink); static const bbsink_ops bbsink_progress_ops = { .begin_backup = bbsink_progress_begin_backup, @@ -48,7 +49,7 @@ static const bbsink_ops bbsink_progress_ops = { .manifest_contents = bbsink_forward_manifest_contents, .end_manifest = bbsink_forward_end_manifest, .end_backup = bbsink_forward_end_backup, - .cleanup = bbsink_forward_cleanup + .cleanup = bbsink_progress_cleanup }; /* @@ -179,6 +180,16 @@ bbsink_progress_archive_contents(bbsink *sink, size_t len) pgstat_progress_update_multi_param(nparam, index, val); } +/* + * Clean up progress reporting. + */ +static void +bbsink_progress_cleanup(bbsink *sink) +{ + pgstat_progress_end_command(); + bbsink_forward_cleanup(sink); +} + /* * Advertise that we are waiting for the start-of-backup checkpoint. */ @@ -231,12 +242,3 @@ basebackup_progress_transfer_wal(void) pgstat_progress_update_param(PROGRESS_BASEBACKUP_PHASE, PROGRESS_BASEBACKUP_PHASE_TRANSFER_WAL); } - -/* - * Advertise that we are no longer performing a backup. - */ -void -basebackup_progress_done(void) -{ - pgstat_progress_end_command(); -} diff --git a/src/include/backup/basebackup_sink.h b/src/include/backup/basebackup_sink.h index 8a5ee996a45..a298a79684d 100644 --- a/src/include/backup/basebackup_sink.h +++ b/src/include/backup/basebackup_sink.h @@ -296,6 +296,5 @@ extern void basebackup_progress_wait_checkpoint(void); extern void basebackup_progress_estimate_backup_size(void); extern void basebackup_progress_wait_wal_archive(bbsink_state *); extern void basebackup_progress_transfer_wal(void); -extern void basebackup_progress_done(void); #endif From 1e1d07792e0827ca84685784c0c958127f5853eb Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 1 Jul 2026 13:27:22 -0400 Subject: [PATCH 108/250] btree_gist: fix NaN handling in float4/float8 opclasses. The float4 and float8 btree_gist opclasses compared keys with raw C operators (==, <, >). IEEE 754 makes every comparison involving NaN false, so GiST disagreed with the regular float comparison operators and with the btree opclass, which uses float[4|8]_cmp_internal() (so that all NaNs are equal and NaN sorts after every non-NaN value). In addition, the penalty and distance functions were not careful about NaNs, and the penalty functions could also misbehave for IEEE infinities. Wrong answers from the penalty functions would probably do no more than make the index non-optimal, but the distance mistakes were visible from SQL. To fix, make the comparison functions rely on the same NaN-aware comparison functions the core code uses, and rewrite the penalty and distance functions to follow the rules that NaNs are equal but maximally far away from non-NaNs. The penalty_num() code was formerly shared between integral and float cases, but I chose to make two copies so that the integral cases are not saddled with the extra logic for NaNs and infinities/overflows. I also rewrote it as static inline functions instead of an unreadable and uncommented macro. The float penalty functions were previously unreached by the regression tests, so add new test cases to exercise them. There's no on-disk format change, but users who have NaN entries in a btree_gist index would be well advised to reindex it. Bug: #19501 Bug: #19524 Reported-by: Man Zeng Reported-by: Yuelin Wang <3020001251@tju.edu.cn> Author: Bill Kim Co-authored-by: Tom Lane Discussion: https://postgr.es/m/19501-3bff3bbc97f1e7c9@postgresql.org Discussion: https://postgr.es/m/19524-9559d302c8455664@postgresql.org Discussion: https://postgr.es/m/CAMQXxcgbtD2LXfX0tpgvOizxP-XxrCHV2ZDy4By_TZnJMsxXWQ@mail.gmail.com Backpatch-through: 14 --- contrib/btree_gist/btree_float4.c | 59 ++++++++--- contrib/btree_gist/btree_float8.c | 51 ++++++--- contrib/btree_gist/btree_utils_num.h | 131 +++++++++++++++++++++--- contrib/btree_gist/data/float4.data | 3 + contrib/btree_gist/data/float8.data | 3 + contrib/btree_gist/expected/float4.out | 51 +++++++-- contrib/btree_gist/expected/float8.out | 51 +++++++-- contrib/btree_gist/expected/numeric.out | 48 ++++----- contrib/btree_gist/sql/float4.sql | 17 +++ contrib/btree_gist/sql/float8.sql | 17 +++ 10 files changed, 345 insertions(+), 86 deletions(-) diff --git a/contrib/btree_gist/btree_float4.c b/contrib/btree_gist/btree_float4.c index bec026a923a..90cfef5742b 100644 --- a/contrib/btree_gist/btree_float4.c +++ b/contrib/btree_gist/btree_float4.c @@ -25,30 +25,36 @@ PG_FUNCTION_INFO_V1(gbt_float4_penalty); PG_FUNCTION_INFO_V1(gbt_float4_same); PG_FUNCTION_INFO_V1(gbt_float4_sortsupport); +/* + * Use the NaN-aware comparators from utils/float.h, so that our results + * will agree with standard btree indexes. Note that penalty and distance + * functions below must also cope with NaNs, in particular with the policy + * that all NaNs are equal. + */ static bool gbt_float4gt(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float4 *) a) > *((const float4 *) b)); + return float4_gt(*((const float4 *) a), *((const float4 *) b)); } static bool gbt_float4ge(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float4 *) a) >= *((const float4 *) b)); + return float4_ge(*((const float4 *) a), *((const float4 *) b)); } static bool gbt_float4eq(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float4 *) a) == *((const float4 *) b)); + return float4_eq(*((const float4 *) a), *((const float4 *) b)); } static bool gbt_float4le(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float4 *) a) <= *((const float4 *) b)); + return float4_le(*((const float4 *) a), *((const float4 *) b)); } static bool gbt_float4lt(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float4 *) a) < *((const float4 *) b)); + return float4_lt(*((const float4 *) a), *((const float4 *) b)); } static int @@ -56,22 +62,33 @@ gbt_float4key_cmp(const void *a, const void *b, FmgrInfo *flinfo) { float4KEY *ia = (float4KEY *) (((const Nsrt *) a)->t); float4KEY *ib = (float4KEY *) (((const Nsrt *) b)->t); + int res; - if (ia->lower == ib->lower) - { - if (ia->upper == ib->upper) - return 0; - - return (ia->upper > ib->upper) ? 1 : -1; - } - - return (ia->lower > ib->lower) ? 1 : -1; + res = float4_cmp_internal(ia->lower, ib->lower); + if (res != 0) + return res; + return float4_cmp_internal(ia->upper, ib->upper); } static float8 gbt_float4_dist(const void *a, const void *b, FmgrInfo *flinfo) { - return GET_FLOAT_DISTANCE(float4, a, b); + float8 arg1 = *(const float4 *) a; + float8 arg2 = *(const float4 *) b; + float8 r; + + r = arg1 - arg2; + /* needn't consider isinf case here, must be due to input infinity */ + if (unlikely(isnan(r))) + { + if (isnan(arg1) && isnan(arg2)) + r = 0.0; /* treat NaNs as equal */ + else if (isnan(arg1) || isnan(arg2)) + r = get_float8_infinity(); /* max dist for NaN vs non-NaN */ + else + r = 0.0; /* must be Inf - Inf case */ + } + return fabs(r); } @@ -101,7 +118,15 @@ float4_dist(PG_FUNCTION_ARGS) r = a - b; if (unlikely(isinf(r)) && !isinf(a) && !isinf(b)) float_overflow_error(); - + if (unlikely(isnan(r))) + { + if (isnan(a) && isnan(b)) + r = 0.0; /* treat NaNs as equal */ + else if (isnan(a) || isnan(b)) + r = get_float4_infinity(); /* max dist for NaN vs non-NaN */ + else + r = 0.0; /* must be Inf - Inf case */ + } PG_RETURN_FLOAT4(fabsf(r)); } @@ -183,7 +208,7 @@ gbt_float4_penalty(PG_FUNCTION_ARGS) float4KEY *newentry = (float4KEY *) DatumGetPointer(((GISTENTRY *) PG_GETARG_POINTER(1))->key); float *result = (float *) PG_GETARG_POINTER(2); - penalty_num(result, origentry->lower, origentry->upper, newentry->lower, newentry->upper); + float_penalty_num(result, origentry->lower, origentry->upper, newentry->lower, newentry->upper); PG_RETURN_POINTER(result); } diff --git a/contrib/btree_gist/btree_float8.c b/contrib/btree_gist/btree_float8.c index 43e7cde2b69..e1df74a6af4 100644 --- a/contrib/btree_gist/btree_float8.c +++ b/contrib/btree_gist/btree_float8.c @@ -26,30 +26,36 @@ PG_FUNCTION_INFO_V1(gbt_float8_same); PG_FUNCTION_INFO_V1(gbt_float8_sortsupport); +/* + * Use the NaN-aware comparators from utils/float.h, so that our results + * will agree with standard btree indexes. Note that penalty and distance + * functions below must also cope with NaNs, in particular with the policy + * that all NaNs are equal. + */ static bool gbt_float8gt(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float8 *) a) > *((const float8 *) b)); + return float8_gt(*((const float8 *) a), *((const float8 *) b)); } static bool gbt_float8ge(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float8 *) a) >= *((const float8 *) b)); + return float8_ge(*((const float8 *) a), *((const float8 *) b)); } static bool gbt_float8eq(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float8 *) a) == *((const float8 *) b)); + return float8_eq(*((const float8 *) a), *((const float8 *) b)); } static bool gbt_float8le(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float8 *) a) <= *((const float8 *) b)); + return float8_le(*((const float8 *) a), *((const float8 *) b)); } static bool gbt_float8lt(const void *a, const void *b, FmgrInfo *flinfo) { - return (*((const float8 *) a) < *((const float8 *) b)); + return float8_lt(*((const float8 *) a), *((const float8 *) b)); } static int @@ -57,16 +63,12 @@ gbt_float8key_cmp(const void *a, const void *b, FmgrInfo *flinfo) { float8KEY *ia = (float8KEY *) (((const Nsrt *) a)->t); float8KEY *ib = (float8KEY *) (((const Nsrt *) b)->t); + int res; - if (ia->lower == ib->lower) - { - if (ia->upper == ib->upper) - return 0; - - return (ia->upper > ib->upper) ? 1 : -1; - } - - return (ia->lower > ib->lower) ? 1 : -1; + res = float8_cmp_internal(ia->lower, ib->lower); + if (res != 0) + return res; + return float8_cmp_internal(ia->upper, ib->upper); } static float8 @@ -79,6 +81,15 @@ gbt_float8_dist(const void *a, const void *b, FmgrInfo *flinfo) r = arg1 - arg2; if (unlikely(isinf(r)) && !isinf(arg1) && !isinf(arg2)) float_overflow_error(); + if (unlikely(isnan(r))) + { + if (isnan(arg1) && isnan(arg2)) + r = 0.0; /* treat NaNs as equal */ + else if (isnan(arg1) || isnan(arg2)) + r = get_float8_infinity(); /* max dist for NaN vs non-NaN */ + else + r = 0.0; /* must be Inf - Inf case */ + } return fabs(r); } @@ -109,7 +120,15 @@ float8_dist(PG_FUNCTION_ARGS) r = a - b; if (unlikely(isinf(r)) && !isinf(a) && !isinf(b)) float_overflow_error(); - + if (unlikely(isnan(r))) + { + if (isnan(a) && isnan(b)) + r = 0.0; /* treat NaNs as equal */ + else if (isnan(a) || isnan(b)) + r = get_float8_infinity(); /* max dist for NaN vs non-NaN */ + else + r = 0.0; /* must be Inf - Inf case */ + } PG_RETURN_FLOAT8(fabs(r)); } @@ -191,7 +210,7 @@ gbt_float8_penalty(PG_FUNCTION_ARGS) float8KEY *newentry = (float8KEY *) DatumGetPointer(((GISTENTRY *) PG_GETARG_POINTER(1))->key); float *result = (float *) PG_GETARG_POINTER(2); - penalty_num(result, origentry->lower, origentry->upper, newentry->lower, newentry->upper); + float_penalty_num(result, origentry->lower, origentry->upper, newentry->lower, newentry->upper); PG_RETURN_POINTER(result); } diff --git a/contrib/btree_gist/btree_utils_num.h b/contrib/btree_gist/btree_utils_num.h index 53e477d8b1e..d42d1e61585 100644 --- a/contrib/btree_gist/btree_utils_num.h +++ b/contrib/btree_gist/btree_utils_num.h @@ -9,6 +9,7 @@ #include "access/gist.h" #include "btree_gist.h" +#include "utils/float.h" typedef char GBT_NUMKEY; @@ -58,21 +59,124 @@ typedef struct /* - * Note: The factor 0.49 in following macro avoids floating point overflows + * Compute penalty for expanding a range olower..oupper to nlower..nupper. + * + * Although the arguments are declared double, they must not be NaN nor + * large enough to risk overflows in the calculations herein. We only + * actually use this for integral data types, so there's no hazard. + */ +static inline float +penalty_num_impl(double olower, double oupper, + double nlower, double nupper, + int natts) +{ + float result = 0.0F; + double tmp = 0.0; + + /* Add penalty for expanding upper bound */ + if (nupper > oupper) + tmp += nupper - oupper; + /* Add penalty for expanding lower bound */ + if (olower > nlower) + tmp += olower - nlower; + if (tmp > 0.0) + { + /* Ensure result is non-zero, even if next step underflows to zero */ + result += FLT_MIN; + /* Scale penalty to 0 .. 1 */ + result += (float) (tmp / (tmp + (oupper - olower))); + /* Scale to 0 .. FLT_MAX / (natts + 1) */ + result *= FLT_MAX / (natts + 1); + } + return result; +} + +/* + * As above, but the input values are float4 or float8, so we must cope + * with NaNs, infinities, and overflows. + */ +static inline float +float_penalty_num_impl(double olower, double oupper, + double nlower, double nupper, + int natts) +{ + float result = 0.0F; + double tmp = 0.0; + + /* Add penalty for expanding upper bound */ + if (float8_gt(nupper, oupper)) + { + double delta = nupper - oupper; + + if (unlikely(isnan(delta))) + { + /* oupper couldn't be NaN here, see float8_gt */ + if (isnan(nupper)) + delta = FLT_MAX; /* max penalty for NaN vs non-NaN */ + else + delta = 0.0; /* must be Inf - Inf case */ + } + else if (delta > FLT_MAX) + delta = FLT_MAX; /* clamp to FLT_MAX, esp for infinity */ + tmp += delta; + } + /* Add penalty for expanding lower bound */ + if (float8_gt(olower, nlower)) + { + double delta = olower - nlower; + + if (unlikely(isnan(delta))) + { + /* nlower couldn't be NaN here, see float8_gt */ + if (isnan(olower)) + delta = FLT_MAX; /* max penalty for NaN vs non-NaN */ + else + delta = 0.0; /* must be Inf - Inf case */ + } + else if (delta > FLT_MAX) + delta = FLT_MAX; /* clamp to FLT_MAX, esp for infinity */ + tmp += delta; + } + if (tmp > 0.0) + { + double delta = oupper - olower; + + /* Clamp delta (the original range size) to 0 .. FLT_MAX */ + if (unlikely(isnan(delta))) + { + /* here, we must deal with olower possibly being NaN */ + if (isnan(oupper) && isnan(olower)) + delta = 0.0; /* treat NaNs as equal */ + else if (isnan(oupper) || isnan(olower)) + delta = FLT_MAX; /* max penalty for NaN vs non-NaN */ + else + delta = 0.0; /* must be Inf - Inf case */ + } + else if (delta > FLT_MAX) + delta = FLT_MAX; /* clamp to FLT_MAX, esp for infinity */ + /* Ensure result is non-zero, even if next step underflows to zero */ + result += FLT_MIN; + /* Scale penalty to 0 .. 1 */ + result += (float) (tmp / (tmp + delta)); + /* Scale to 0 .. FLT_MAX / (natts + 1) */ + result *= FLT_MAX / (natts + 1); + } + return result; +} + +/* + * These macros provide backwards-compatible notation for callers. */ #define penalty_num(result,olower,oupper,nlower,nupper) do { \ - double tmp = 0.0F; \ - (*(result)) = 0.0F; \ - if ( (nupper) > (oupper) ) \ - tmp += ( ((double)nupper)*0.49F - ((double)oupper)*0.49F ); \ - if ( (olower) > (nlower) ) \ - tmp += ( ((double)olower)*0.49F - ((double)nlower)*0.49F ); \ - if (tmp > 0.0F) \ - { \ - (*(result)) += FLT_MIN; \ - (*(result)) += (float) ( ((double)(tmp)) / ( (double)(tmp) + ( ((double)(oupper))*0.49F - ((double)(olower))*0.49F ) ) ); \ - (*(result)) *= (FLT_MAX / (((GISTENTRY *) PG_GETARG_POINTER(0))->rel->rd_att->natts + 1)); \ - } \ + GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); \ + *(result) = penalty_num_impl(olower, oupper, nlower, nupper, \ + entry->rel->rd_att->natts); \ +} while (0) + +#define float_penalty_num(result,olower,oupper,nlower,nupper) do { \ + GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); \ + *(result) = float_penalty_num_impl(olower, oupper, nlower, nupper, \ + entry->rel->rd_att->natts); \ } while (0) @@ -86,6 +190,7 @@ typedef struct (ivp)->day * (24.0 * SECS_PER_HOUR) + \ (ivp)->month * (30.0 * SECS_PER_DAY)) +/* This macro is not safe to use with actual float inputs, only integers */ #define GET_FLOAT_DISTANCE(t, arg1, arg2) fabs( ((float8) *((const t *) (arg1))) - ((float8) *((const t *) (arg2))) ) diff --git a/contrib/btree_gist/data/float4.data b/contrib/btree_gist/data/float4.data index 947955e4680..af7d09f00d6 100644 --- a/contrib/btree_gist/data/float4.data +++ b/contrib/btree_gist/data/float4.data @@ -298,6 +298,9 @@ \N 2972.381398 220.199877 +Infinity +-Infinity +NaN 3542.561032 -2168.024176 -3305.714558 diff --git a/contrib/btree_gist/data/float8.data b/contrib/btree_gist/data/float8.data index ff21226e066..b60e22f957f 100644 --- a/contrib/btree_gist/data/float8.data +++ b/contrib/btree_gist/data/float8.data @@ -298,6 +298,9 @@ 27770.539968 13275.355549 -4267.695804 +Infinity +-Infinity +NaN \N \N 38915.525185 diff --git a/contrib/btree_gist/expected/float4.out b/contrib/btree_gist/expected/float4.out index dfe732049e6..a917b79a63b 100644 --- a/contrib/btree_gist/expected/float4.out +++ b/contrib/btree_gist/expected/float4.out @@ -5,13 +5,13 @@ SET enable_seqscan=on; SELECT count(*) FROM float4tmp WHERE a < -179.0; count ------- - 244 + 245 (1 row) SELECT count(*) FROM float4tmp WHERE a <= -179.0; count ------- - 245 + 246 (1 row) SELECT count(*) FROM float4tmp WHERE a = -179.0; @@ -23,13 +23,13 @@ SELECT count(*) FROM float4tmp WHERE a = -179.0; SELECT count(*) FROM float4tmp WHERE a >= -179.0; count ------- - 303 + 305 (1 row) SELECT count(*) FROM float4tmp WHERE a > -179.0; count ------- - 302 + 304 (1 row) SELECT a, a <-> '-179.0' FROM float4tmp ORDER BY a <-> '-179.0' LIMIT 3; @@ -45,13 +45,13 @@ SET enable_seqscan=off; SELECT count(*) FROM float4tmp WHERE a < -179.0::float4; count ------- - 244 + 245 (1 row) SELECT count(*) FROM float4tmp WHERE a <= -179.0::float4; count ------- - 245 + 246 (1 row) SELECT count(*) FROM float4tmp WHERE a = -179.0::float4; @@ -63,13 +63,13 @@ SELECT count(*) FROM float4tmp WHERE a = -179.0::float4; SELECT count(*) FROM float4tmp WHERE a >= -179.0::float4; count ------- - 303 + 305 (1 row) SELECT count(*) FROM float4tmp WHERE a > -179.0::float4; count ------- - 302 + 304 (1 row) EXPLAIN (COSTS OFF) @@ -89,3 +89,38 @@ SELECT a, a <-> '-179.0' FROM float4tmp ORDER BY a <-> '-179.0' LIMIT 3; -158.17741 | 20.822586 (3 rows) +-- EXCLUDE constraint must block a duplicate NaN, same as it does for finite +-- values. +CREATE TABLE float4excl (a float4, EXCLUDE USING gist (a WITH =)); +INSERT INTO float4excl VALUES ('NaN'::float4); +INSERT INTO float4excl VALUES ('NaN'::float4); -- expect: violates EXCLUDE +ERROR: conflicting key value violates exclusion constraint "float4excl_a_excl" +DETAIL: Key (a)=(NaN) conflicts with existing key (a)=(NaN). +SELECT count(*) FROM float4excl; + count +------- + 1 +(1 row) + +-- Test double-column index +CREATE INDEX float4idx2 ON float4tmp USING gist ( a, abs(a) ); +EXPLAIN (COSTS OFF) +SELECT count(*) FROM float4tmp WHERE abs(a) = 179.0::float4; + QUERY PLAN +-------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on float4tmp + Recheck Cond: (abs(a) = '179'::real) + -> Bitmap Index Scan on float4idx2 + Index Cond: (abs(a) = '179'::real) +(5 rows) + +SELECT count(*) FROM float4tmp WHERE abs(a) = 179.0::float4; + count +------- + 1 +(1 row) + +RESET enable_seqscan; +RESET enable_indexscan; +RESET enable_bitmapscan; diff --git a/contrib/btree_gist/expected/float8.out b/contrib/btree_gist/expected/float8.out index ebd0ef3d689..194bd210ac6 100644 --- a/contrib/btree_gist/expected/float8.out +++ b/contrib/btree_gist/expected/float8.out @@ -5,13 +5,13 @@ SET enable_seqscan=on; SELECT count(*) FROM float8tmp WHERE a < -1890.0; count ------- - 237 + 238 (1 row) SELECT count(*) FROM float8tmp WHERE a <= -1890.0; count ------- - 238 + 239 (1 row) SELECT count(*) FROM float8tmp WHERE a = -1890.0; @@ -23,13 +23,13 @@ SELECT count(*) FROM float8tmp WHERE a = -1890.0; SELECT count(*) FROM float8tmp WHERE a >= -1890.0; count ------- - 307 + 309 (1 row) SELECT count(*) FROM float8tmp WHERE a > -1890.0; count ------- - 306 + 308 (1 row) SELECT a, a <-> '-1890.0' FROM float8tmp ORDER BY a <-> '-1890.0' LIMIT 3; @@ -45,13 +45,13 @@ SET enable_seqscan=off; SELECT count(*) FROM float8tmp WHERE a < -1890.0::float8; count ------- - 237 + 238 (1 row) SELECT count(*) FROM float8tmp WHERE a <= -1890.0::float8; count ------- - 238 + 239 (1 row) SELECT count(*) FROM float8tmp WHERE a = -1890.0::float8; @@ -63,13 +63,13 @@ SELECT count(*) FROM float8tmp WHERE a = -1890.0::float8; SELECT count(*) FROM float8tmp WHERE a >= -1890.0::float8; count ------- - 307 + 309 (1 row) SELECT count(*) FROM float8tmp WHERE a > -1890.0::float8; count ------- - 306 + 308 (1 row) EXPLAIN (COSTS OFF) @@ -89,3 +89,38 @@ SELECT a, a <-> '-1890.0' FROM float8tmp ORDER BY a <-> '-1890.0' LIMIT 3; -1769.73634 | 120.26366000000007 (3 rows) +-- EXCLUDE constraint must block a duplicate NaN, same as it does for finite +-- values. +CREATE TABLE float8excl (a float8, EXCLUDE USING gist (a WITH =)); +INSERT INTO float8excl VALUES ('NaN'::float8); +INSERT INTO float8excl VALUES ('NaN'::float8); -- expect: violates EXCLUDE +ERROR: conflicting key value violates exclusion constraint "float8excl_a_excl" +DETAIL: Key (a)=(NaN) conflicts with existing key (a)=(NaN). +SELECT count(*) FROM float8excl; + count +------- + 1 +(1 row) + +-- Test double-column index +CREATE INDEX float8idx2 ON float8tmp USING gist ( a, abs(a) ); +EXPLAIN (COSTS OFF) +SELECT count(*) FROM float8tmp WHERE abs(a) = 1890.0::float8; + QUERY PLAN +--------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on float8tmp + Recheck Cond: (abs(a) = '1890'::double precision) + -> Bitmap Index Scan on float8idx2 + Index Cond: (abs(a) = '1890'::double precision) +(5 rows) + +SELECT count(*) FROM float8tmp WHERE abs(a) = 1890.0::float8; + count +------- + 1 +(1 row) + +RESET enable_seqscan; +RESET enable_indexscan; +RESET enable_bitmapscan; diff --git a/contrib/btree_gist/expected/numeric.out b/contrib/btree_gist/expected/numeric.out index ae839b8ec83..34c1e568063 100644 --- a/contrib/btree_gist/expected/numeric.out +++ b/contrib/btree_gist/expected/numeric.out @@ -7,13 +7,13 @@ SET enable_seqscan=on; SELECT count(*) FROM numerictmp WHERE a < -1890.0; count ------- - 505 + 506 (1 row) SELECT count(*) FROM numerictmp WHERE a <= -1890.0; count ------- - 506 + 507 (1 row) SELECT count(*) FROM numerictmp WHERE a = -1890.0; @@ -25,37 +25,37 @@ SELECT count(*) FROM numerictmp WHERE a = -1890.0; SELECT count(*) FROM numerictmp WHERE a >= -1890.0; count ------- - 597 + 599 (1 row) SELECT count(*) FROM numerictmp WHERE a > -1890.0; count ------- - 596 + 598 (1 row) SELECT count(*) FROM numerictmp WHERE a < 'NaN' ; count ------- - 1100 + 1102 (1 row) SELECT count(*) FROM numerictmp WHERE a <= 'NaN' ; count ------- - 1102 + 1105 (1 row) SELECT count(*) FROM numerictmp WHERE a = 'NaN' ; count ------- - 2 + 3 (1 row) SELECT count(*) FROM numerictmp WHERE a >= 'NaN' ; count ------- - 2 + 3 (1 row) SELECT count(*) FROM numerictmp WHERE a > 'NaN' ; @@ -67,13 +67,13 @@ SELECT count(*) FROM numerictmp WHERE a > 'NaN' ; SELECT count(*) FROM numerictmp WHERE a < 0 ; count ------- - 523 + 524 (1 row) SELECT count(*) FROM numerictmp WHERE a <= 0 ; count ------- - 526 + 527 (1 row) SELECT count(*) FROM numerictmp WHERE a = 0 ; @@ -85,13 +85,13 @@ SELECT count(*) FROM numerictmp WHERE a = 0 ; SELECT count(*) FROM numerictmp WHERE a >= 0 ; count ------- - 579 + 581 (1 row) SELECT count(*) FROM numerictmp WHERE a > 0 ; count ------- - 576 + 578 (1 row) CREATE INDEX numericidx ON numerictmp USING gist ( a ); @@ -99,13 +99,13 @@ SET enable_seqscan=off; SELECT count(*) FROM numerictmp WHERE a < -1890.0; count ------- - 505 + 506 (1 row) SELECT count(*) FROM numerictmp WHERE a <= -1890.0; count ------- - 506 + 507 (1 row) SELECT count(*) FROM numerictmp WHERE a = -1890.0; @@ -117,37 +117,37 @@ SELECT count(*) FROM numerictmp WHERE a = -1890.0; SELECT count(*) FROM numerictmp WHERE a >= -1890.0; count ------- - 597 + 599 (1 row) SELECT count(*) FROM numerictmp WHERE a > -1890.0; count ------- - 596 + 598 (1 row) SELECT count(*) FROM numerictmp WHERE a < 'NaN' ; count ------- - 1100 + 1102 (1 row) SELECT count(*) FROM numerictmp WHERE a <= 'NaN' ; count ------- - 1102 + 1105 (1 row) SELECT count(*) FROM numerictmp WHERE a = 'NaN' ; count ------- - 2 + 3 (1 row) SELECT count(*) FROM numerictmp WHERE a >= 'NaN' ; count ------- - 2 + 3 (1 row) SELECT count(*) FROM numerictmp WHERE a > 'NaN' ; @@ -159,13 +159,13 @@ SELECT count(*) FROM numerictmp WHERE a > 'NaN' ; SELECT count(*) FROM numerictmp WHERE a < 0 ; count ------- - 523 + 524 (1 row) SELECT count(*) FROM numerictmp WHERE a <= 0 ; count ------- - 526 + 527 (1 row) SELECT count(*) FROM numerictmp WHERE a = 0 ; @@ -177,13 +177,13 @@ SELECT count(*) FROM numerictmp WHERE a = 0 ; SELECT count(*) FROM numerictmp WHERE a >= 0 ; count ------- - 579 + 581 (1 row) SELECT count(*) FROM numerictmp WHERE a > 0 ; count ------- - 576 + 578 (1 row) -- Test index-only scans diff --git a/contrib/btree_gist/sql/float4.sql b/contrib/btree_gist/sql/float4.sql index 3da1ce953c8..71de5d5cf49 100644 --- a/contrib/btree_gist/sql/float4.sql +++ b/contrib/btree_gist/sql/float4.sql @@ -35,3 +35,20 @@ SELECT count(*) FROM float4tmp WHERE a > -179.0::float4; EXPLAIN (COSTS OFF) SELECT a, a <-> '-179.0' FROM float4tmp ORDER BY a <-> '-179.0' LIMIT 3; SELECT a, a <-> '-179.0' FROM float4tmp ORDER BY a <-> '-179.0' LIMIT 3; + +-- EXCLUDE constraint must block a duplicate NaN, same as it does for finite +-- values. +CREATE TABLE float4excl (a float4, EXCLUDE USING gist (a WITH =)); +INSERT INTO float4excl VALUES ('NaN'::float4); +INSERT INTO float4excl VALUES ('NaN'::float4); -- expect: violates EXCLUDE +SELECT count(*) FROM float4excl; + +-- Test double-column index +CREATE INDEX float4idx2 ON float4tmp USING gist ( a, abs(a) ); +EXPLAIN (COSTS OFF) +SELECT count(*) FROM float4tmp WHERE abs(a) = 179.0::float4; +SELECT count(*) FROM float4tmp WHERE abs(a) = 179.0::float4; + +RESET enable_seqscan; +RESET enable_indexscan; +RESET enable_bitmapscan; diff --git a/contrib/btree_gist/sql/float8.sql b/contrib/btree_gist/sql/float8.sql index e1e819b37f9..a0fc84f94bb 100644 --- a/contrib/btree_gist/sql/float8.sql +++ b/contrib/btree_gist/sql/float8.sql @@ -35,3 +35,20 @@ SELECT count(*) FROM float8tmp WHERE a > -1890.0::float8; EXPLAIN (COSTS OFF) SELECT a, a <-> '-1890.0' FROM float8tmp ORDER BY a <-> '-1890.0' LIMIT 3; SELECT a, a <-> '-1890.0' FROM float8tmp ORDER BY a <-> '-1890.0' LIMIT 3; + +-- EXCLUDE constraint must block a duplicate NaN, same as it does for finite +-- values. +CREATE TABLE float8excl (a float8, EXCLUDE USING gist (a WITH =)); +INSERT INTO float8excl VALUES ('NaN'::float8); +INSERT INTO float8excl VALUES ('NaN'::float8); -- expect: violates EXCLUDE +SELECT count(*) FROM float8excl; + +-- Test double-column index +CREATE INDEX float8idx2 ON float8tmp USING gist ( a, abs(a) ); +EXPLAIN (COSTS OFF) +SELECT count(*) FROM float8tmp WHERE abs(a) = 1890.0::float8; +SELECT count(*) FROM float8tmp WHERE abs(a) = 1890.0::float8; + +RESET enable_seqscan; +RESET enable_indexscan; +RESET enable_bitmapscan; From 84001a04d552ffd00863a3a9f67f42fc8cb0b677 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 2 Jul 2026 12:44:33 +0900 Subject: [PATCH 109/250] Fix jsonpath .decimal() to honor silent mode The jsonpath .decimal(precision[, scale]) method built its numeric typmod by calling numerictypmodin() through DirectFunctionCall1(), which can throw a hard error for an incorrect set of precision and/or scale vaulues. This breaks the silent mode supported by this function, that should not fail. Most of the jsonpath code uses the soft error reporting to bypass errors, which is what this fix does by avoiding a direct use of numerictypmodin(). Its code is refactored to use a new routine called make_numeric_typmod_safe(), able to take an error context in input. numerictypmodin() sets no context, mapping to its previous behavior. The jsonpath code sets or not a context depending on the use of the silent mode. This result leads to some nice simplifications: numerictypmodin() feeds on an array, we can now pass directly values for the scale and precision. Oversight in 66ea94e8e606. Author: Ewan Young Discussion: https://postgr.es/m/CAON2xHMaigKABiyPBBq3Sjd3gp7uWMJXnnMHt=s85V1ij3KP1w@mail.gmail.com Backpatch-through: 17 --- src/backend/utils/adt/jsonpath_exec.c | 24 +++-------- src/backend/utils/adt/numeric.c | 44 +++++++++++--------- src/include/utils/numeric.h | 5 +++ src/test/regress/expected/jsonb_jsonpath.out | 32 ++++++++++++++ src/test/regress/sql/jsonb_jsonpath.sql | 7 ++++ 5 files changed, 75 insertions(+), 37 deletions(-) diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eedc5c87b05..4be288c23eb 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -1463,15 +1463,11 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, if (jsp->type == jpiDecimal && jsp->content.args.left) { Datum numdatum; - Datum dtypmod; + int32 dtypmod; int32 precision; int32 scale = 0; bool have_error; bool noerr; - ArrayType *arrtypmod; - Datum datums[2]; - char pstr[12]; /* sign, 10 digits and '\0' */ - char sstr[12]; /* sign, 10 digits and '\0' */ ErrorSaveContext escontext = {T_ErrorSaveContext}; jspGetLeftArg(jsp, &elem); @@ -1501,18 +1497,11 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, jspOperationName(jsp->type))))); } - /* - * numerictypmodin() takes the precision and scale in the - * form of CString arrays. - */ - pg_ltoa(precision, pstr); - datums[0] = CStringGetDatum(pstr); - pg_ltoa(scale, sstr); - datums[1] = CStringGetDatum(sstr); - arrtypmod = construct_array_builtin(datums, 2, CSTRINGOID); - - dtypmod = DirectFunctionCall1(numerictypmodin, - PointerGetDatum(arrtypmod)); + /* Pack the precision and scale into a numeric typmod */ + dtypmod = make_numeric_typmod_safe(precision, scale, + jspThrowErrors(cxt) ? NULL : (Node *) &escontext); + if (escontext.error_occurred) + return jperError; /* Convert numstr to Numeric with typmod */ Assert(numstr != NULL); @@ -1528,7 +1517,6 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, numstr, jspOperationName(jsp->type), "numeric")))); num = DatumGetNumeric(numdatum); - pfree(arrtypmod); } jb = &jbv; diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c index 58ad1a65ef7..e21c08c85a2 100644 --- a/src/backend/utils/adt/numeric.c +++ b/src/backend/utils/adt/numeric.c @@ -1320,6 +1320,29 @@ numeric (PG_FUNCTION_ARGS) PG_RETURN_NUMERIC(new); } +/* + * make_numeric_typmod_safe() - + * + * Validate a numeric precision/scale and pack them into a typmod value, + * with soft error handling. + */ +int32 +make_numeric_typmod_safe(int32 precision, int32 scale, Node *escontext) +{ + if (precision < 1 || precision > NUMERIC_MAX_PRECISION) + ereturn(escontext, -1, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("NUMERIC precision %d must be between 1 and %d", + precision, NUMERIC_MAX_PRECISION))); + if (scale < NUMERIC_MIN_SCALE || scale > NUMERIC_MAX_SCALE) + ereturn(escontext, -1, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("NUMERIC scale %d must be between %d and %d", + scale, NUMERIC_MIN_SCALE, NUMERIC_MAX_SCALE))); + + return make_numeric_typmod(precision, scale); +} + Datum numerictypmodin(PG_FUNCTION_ARGS) { @@ -1331,28 +1354,11 @@ numerictypmodin(PG_FUNCTION_ARGS) tl = ArrayGetIntegerTypmods(ta, &n); if (n == 2) - { - if (tl[0] < 1 || tl[0] > NUMERIC_MAX_PRECISION) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("NUMERIC precision %d must be between 1 and %d", - tl[0], NUMERIC_MAX_PRECISION))); - if (tl[1] < NUMERIC_MIN_SCALE || tl[1] > NUMERIC_MAX_SCALE) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("NUMERIC scale %d must be between %d and %d", - tl[1], NUMERIC_MIN_SCALE, NUMERIC_MAX_SCALE))); - typmod = make_numeric_typmod(tl[0], tl[1]); - } + typmod = make_numeric_typmod_safe(tl[0], tl[1], NULL); else if (n == 1) { - if (tl[0] < 1 || tl[0] > NUMERIC_MAX_PRECISION) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("NUMERIC precision %d must be between 1 and %d", - tl[0], NUMERIC_MAX_PRECISION))); /* scale defaults to zero */ - typmod = make_numeric_typmod(tl[0], 0); + typmod = make_numeric_typmod_safe(tl[0], 0, NULL); } else { diff --git a/src/include/utils/numeric.h b/src/include/utils/numeric.h index 9e79fc376cb..a8a4dac4596 100644 --- a/src/include/utils/numeric.h +++ b/src/include/utils/numeric.h @@ -17,6 +17,9 @@ #include "common/pg_prng.h" #include "fmgr.h" +/* forward declaration to avoid node.h include */ +typedef struct Node Node; + /* * Limits on the precision and scale specifiable in a NUMERIC typmod. The * precision is strictly positive, but the scale may be positive or negative. @@ -103,6 +106,8 @@ extern Numeric numeric_mod_opt_error(Numeric num1, Numeric num2, bool *have_error); extern int32 numeric_int4_opt_error(Numeric num, bool *have_error); extern int64 numeric_int8_opt_error(Numeric num, bool *have_error); +extern int32 make_numeric_typmod_safe(int32 precision, int32 scale, + Node *escontext); extern Numeric random_numeric(pg_prng_state *state, Numeric rmin, Numeric rmax); diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out index 4bcd4e91a29..f3b605d5926 100644 --- a/src/test/regress/expected/jsonb_jsonpath.out +++ b/src/test/regress/expected/jsonb_jsonpath.out @@ -2336,6 +2336,38 @@ select jsonb_path_query('12.3', '$.decimal(12345678901,1)'); ERROR: precision of jsonpath item method .decimal() is out of range for type integer select jsonb_path_query('12.3', '$.decimal(1,12345678901)'); ERROR: scale of jsonpath item method .decimal() is out of range for type integer +-- An out-of-range precision or scale does not fail in silent mode. +select jsonb_path_query('12345.678', '$.decimal(0, 6)', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('12345.678', '$.decimal(1001, 6)', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('1234.5678', '$.decimal(-6, +2)', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('1234.5678', '$.decimal(6, -1001)', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('1234.5678', '$.decimal(6, 1001)', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select '1234.5678'::jsonb @? '$.decimal(0)'; + ?column? +---------- + +(1 row) + -- Test .integer() select jsonb_path_query('null', '$.integer()'); ERROR: jsonpath item method .integer() can only be applied to a string or numeric value diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql index 3e8929a5269..6fa4a5c0b4e 100644 --- a/src/test/regress/sql/jsonb_jsonpath.sql +++ b/src/test/regress/sql/jsonb_jsonpath.sql @@ -523,6 +523,13 @@ select jsonb_path_query('0.0012345', '$.decimal(2,4)'); select jsonb_path_query('-0.00123456', '$.decimal(2,-4)'); select jsonb_path_query('12.3', '$.decimal(12345678901,1)'); select jsonb_path_query('12.3', '$.decimal(1,12345678901)'); +-- An out-of-range precision or scale does not fail in silent mode. +select jsonb_path_query('12345.678', '$.decimal(0, 6)', silent => true); +select jsonb_path_query('12345.678', '$.decimal(1001, 6)', silent => true); +select jsonb_path_query('1234.5678', '$.decimal(-6, +2)', silent => true); +select jsonb_path_query('1234.5678', '$.decimal(6, -1001)', silent => true); +select jsonb_path_query('1234.5678', '$.decimal(6, 1001)', silent => true); +select '1234.5678'::jsonb @? '$.decimal(0)'; -- Test .integer() select jsonb_path_query('null', '$.integer()'); From 90789900b84a71d9c21d46220dd2e2a2477d3322 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 2 Jul 2026 15:06:05 +0900 Subject: [PATCH 110/250] Fix redefinition of typedef Node in numeric.h Commit 84001a04d552 has added a forward declaration of Node, something not allowed in C99. Per buildfarm members longfin and sifaka. Discussion: https://postgr.es/m/akXt_WYx0dgdH6rf@paquier.xyz Backpatch-through: 17-18 --- src/include/utils/numeric.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/include/utils/numeric.h b/src/include/utils/numeric.h index a8a4dac4596..d38c9d58a34 100644 --- a/src/include/utils/numeric.h +++ b/src/include/utils/numeric.h @@ -18,7 +18,7 @@ #include "fmgr.h" /* forward declaration to avoid node.h include */ -typedef struct Node Node; +struct Node; /* * Limits on the precision and scale specifiable in a NUMERIC typmod. The @@ -107,7 +107,7 @@ extern Numeric numeric_mod_opt_error(Numeric num1, Numeric num2, extern int32 numeric_int4_opt_error(Numeric num, bool *have_error); extern int64 numeric_int8_opt_error(Numeric num, bool *have_error); extern int32 make_numeric_typmod_safe(int32 precision, int32 scale, - Node *escontext); + struct Node *escontext); extern Numeric random_numeric(pg_prng_state *state, Numeric rmin, Numeric rmax); From c8d68bfd52d70d7b416c0eb775df29634c583fef Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 3 Jul 2026 11:16:34 +0900 Subject: [PATCH 111/250] Remove replication slot advice from MultiXact wraparound hints Previously, MultiXactId wraparound hints suggested dropping stale replication slots. While that advice is appropriate for transaction ID wraparound, where replication slots can hold back XID horizons, it was misleading for MultiXactId wraparound. Following it could lead users to drop replication slots unnecessarily without helping resolve the MultiXactId wraparound condition. MultiXact cleanup is not directly delayed by replication slots. Instead, it depends on whether old MultiXactIds can still be seen as live by running transactions. This commit removes the replication slot advice from MultiXactId wraparound hints, and documents that stale replication slots are normally not relevant to resolving MultiXactId wraparound problems. Backpatch to all supported branches. BUG #18876 Reported-by: Haruka Takatsuka Author: Fujii Masao Discussion: https://postgr.es/m/18876-0d0b53bad5a1f4c1@postgresql.org Backpatch-through: 14 --- doc/src/sgml/maintenance.sgml | 6 ++++++ src/backend/access/transam/multixact.c | 12 ++++++------ src/backend/commands/vacuum.c | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index ba0d338b48f..282199ca033 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -839,6 +839,12 @@ HINT: Execute a database-wide VACUUM in that database. Running transactions and prepared transactions can be ignored if there is no chance that they might appear in a multixact. + + Unlike transaction ID wraparound, replication slots do not + directly hold back multixact cleanup. Dropping stale replication + slots is therefore not usually relevant to resolving multixact ID + wraparound problems. + MXID information is not directly visible in system views such as pg_stat_activity; however, looking for old XIDs is still a good diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index da2a174d98f..494fb196eef 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -1266,14 +1266,14 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) errmsg("database is not accepting commands that assign new MultiXactIds to avoid wraparound data loss in database \"%s\"", oldest_datname), errhint("Execute a database-wide VACUUM in that database.\n" - "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); + "You might also need to commit or roll back old prepared transactions."))); else ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("database is not accepting commands that assign new MultiXactIds to avoid wraparound data loss in database with OID %u", oldest_datoid), errhint("Execute a database-wide VACUUM in that database.\n" - "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); + "You might also need to commit or roll back old prepared transactions."))); } /* @@ -1297,7 +1297,7 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) oldest_datname, multiWrapLimit - result), errhint("Execute a database-wide VACUUM in that database.\n" - "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); + "You might also need to commit or roll back old prepared transactions."))); else ereport(WARNING, (errmsg_plural("database with OID %u must be vacuumed before %u more MultiXactId is used", @@ -1306,7 +1306,7 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) oldest_datoid, multiWrapLimit - result), errhint("Execute a database-wide VACUUM in that database.\n" - "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); + "You might also need to commit or roll back old prepared transactions."))); } /* Re-acquire lock and start over */ @@ -2653,7 +2653,7 @@ SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid, oldest_datname, multiWrapLimit - curMulti), errhint("To avoid MultiXactId assignment failures, execute a database-wide VACUUM in that database.\n" - "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); + "You might also need to commit or roll back old prepared transactions."))); else ereport(WARNING, (errmsg_plural("database with OID %u must be vacuumed before %u more MultiXactId is used", @@ -2662,7 +2662,7 @@ SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid, oldest_datoid, multiWrapLimit - curMulti), errhint("To avoid MultiXactId assignment failures, execute a database-wide VACUUM in that database.\n" - "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); + "You might also need to commit or roll back old prepared transactions."))); } } diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index be863db81cb..934b645efd6 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -1187,7 +1187,7 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params, ereport(WARNING, (errmsg("cutoff for freezing multixacts is far in the past"), errhint("Close open transactions soon to avoid wraparound problems.\n" - "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); + "You might also need to commit or roll back old prepared transactions."))); /* * Determine the minimum freeze age to use: as specified by the caller, or From 598af79b1b5e1b62c5409a8e6f7b3126848d17c8 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 3 Jul 2026 13:46:35 +0900 Subject: [PATCH 112/250] psql: Fix \df tab completion for procedures Commit fb421231daa extended \df to include procedures, but its tab completion continued not to show procedures. Update \df tab completion to include procedures as well. Backpatch to all supported versions. Author: Erik Wienhold Reviewed-by: Surya Poondla Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/10fbfdfe-80f6-4ef9-b8b3-f7be0eb53a50@ewie.name Backpatch-through: 14 --- src/bin/psql/tab-complete.in.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index fdd7e2308ca..81b9e203b5c 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -5300,7 +5300,7 @@ match_previous_words(int pattern_id, else if (TailMatchesCS("\\dew*")) COMPLETE_WITH_QUERY(Query_for_list_of_fdws); else if (TailMatchesCS("\\df*")) - COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines); else if (HeadMatchesCS("\\df*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); From dd5eca055d484af41eb6d50d6eeea7cb065d450c Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Fri, 3 Jul 2026 14:57:35 +0300 Subject: [PATCH 113/250] Fix tracing of BackendKeyData and CancelRequest BackendKeyData length was increased from 4 bytes to a variable-length length (up to 256 bytes) in a460251f0a. However, pqTrace still traces it as a 4 bytes key, leading to a "mismatched message length" warning message. The same issue impacts the tracing of CancelRequest. This patch fixes the issue by using pqTraceOutputNchar instead of pqTraceOutputInt32 in both cases. Author: Anthonin Bonnefoy Discussion: https://www.postgresql.org/message-id/CAO6_Xqo6gTv9=76H=k2qDRFU+KHuBiY2S=bQynEr6J8gS7L6xA@mail.gmail.com Backpatch-through: 18 --- src/interfaces/libpq/fe-trace.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index fae5b47e551..f97319c5d96 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -452,11 +452,12 @@ pqTraceOutput_CopyOutResponse(FILE *f, const char *message, int *cursor) } static void -pqTraceOutput_BackendKeyData(FILE *f, const char *message, int *cursor, bool regress) +pqTraceOutput_BackendKeyData(FILE *f, const char *message, int *cursor, int length, + bool regress) { fprintf(f, "BackendKeyData\t"); pqTraceOutputInt32(f, message, cursor, regress); - pqTraceOutputInt32(f, message, cursor, regress); + pqTraceOutputNchar(f, length - *cursor + 1, message, cursor, regress); } static void @@ -762,7 +763,8 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) /* No message content */ break; case PqMsg_BackendKeyData: - pqTraceOutput_BackendKeyData(conn->Pfdebug, message, &logCursor, regress); + pqTraceOutput_BackendKeyData(conn->Pfdebug, message, &logCursor, + length, regress); break; case PqMsg_NoData: fprintf(conn->Pfdebug, "NoData"); @@ -876,7 +878,8 @@ pqTraceOutputNoTypeByteMessage(PGconn *conn, const char *message) pqTraceOutputInt16(conn->Pfdebug, message, &logCursor); pqTraceOutputInt16(conn->Pfdebug, message, &logCursor); pqTraceOutputInt32(conn->Pfdebug, message, &logCursor, regress); - pqTraceOutputInt32(conn->Pfdebug, message, &logCursor, regress); + pqTraceOutputNchar(conn->Pfdebug, length - logCursor, message, + &logCursor, regress); } else if (version == NEGOTIATE_SSL_CODE) { From 3aaefe8924f4828eabd414bfda98793e769aed8e Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Fri, 3 Jul 2026 15:53:03 +0300 Subject: [PATCH 114/250] Prevent access to other sessions' empty temp tables Commit ce146621 ensures that ERROR is raised if a session tries to read pages of another session's temp table. But there is a corner case where the other session's temp table is empty -- in this case the INSERT command bypasses our checks and executes without any errors. Such behavior is inconsistent and erroneous: it leaves an invalid buffer in the temp buffers pool. Since the buffer was created for another session's temp table, we get an error "no such file or directory" when trying to flush it. This commit fixes it by adding a RELATION_IS_OTHER_TEMP check in the relation-extension path. Backpatch to 16, because it is the first release after 31966b151e6, which introduced a separate local relation extension function ExtendBufferedRelLocal(), which lacks of RELATION_IS_OTHER_TEMP() check. As this fix introduces more checks to 013_temp_obj_multisession.pl, backpatch the whole test script to 16. Discussion: https://postgr.es/m/CAJDiXgiX2XZBHDNo%2BzBbvku%2BtchrUurvPRaN1_40mEQ1_sG90g%40mail.gmail.com Author: Daniil Davydov <3danissimo@gmail.com> Reviewed-by: Jim Jones Reviewed-by: Imran Zaheer Reviewed-by: ZizhuanLiu X-MAN <44973863@qq.com> Backpatch-through: 16 --- src/backend/storage/buffer/bufmgr.c | 14 ++++++++++++++ src/include/utils/rel.h | 8 ++++---- .../test_misc/t/013_temp_obj_multisession.pl | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 27fd7e9720a..a5b4bc5b7d5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -2586,9 +2586,23 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr, extend_by); if (bmr.relpersistence == RELPERSISTENCE_TEMP) + { + /* + * Reject attempts to extend non-local temporary relations; we have no + * ability to transfer about-to-be-created local buffers into the + * owning session's local buffers. This is the canonical place for + * the check, covering any attempt to extend a non-local temporary + * relation. + */ + if (bmr.rel && RELATION_IS_OTHER_TEMP(bmr.rel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot access temporary tables of other sessions"))); + first_block = ExtendBufferedRelLocal(bmr, fork, flags, extend_by, extend_upto, buffers, &extend_by); + } else first_block = ExtendBufferedRelShared(bmr, fork, strategy, flags, extend_by, extend_upto, diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 2f86c79a907..09c794239ca 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -668,10 +668,10 @@ RelationCloseSmgr(Relation relation) * the owning session keeps the data in its private local buffer pool, * which we cannot access. Existing buffer-manager entry points * (ReadBuffer_common(), StartReadBuffersImpl(), read_stream_begin_impl(), - * and PrefetchBuffer()) already enforce this; any new buffer-access entry - * points must do the same. Command-level code (TRUNCATE, ALTER TABLE, - * VACUUM, CLUSTER, REINDEX, ...) additionally uses this macro for - * command-specific error messages. + * PrefetchBuffer() and ExtendBufferedRelCommon()) already enforce this; any + * new buffer-access entry points must do the same. Command-level code + * (TRUNCATE, ALTER TABLE, VACUUM, CLUSTER, REINDEX, ...) additionally uses + * this macro for command-specific error messages. * * Beware of multiple eval of argument */ diff --git a/src/test/modules/test_misc/t/013_temp_obj_multisession.pl b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl index c56a032e57f..0e28ed59d05 100644 --- a/src/test/modules/test_misc/t/013_temp_obj_multisession.pl +++ b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl @@ -37,6 +37,10 @@ # masked by an index scan that would hit ReadBuffer_common from nbtree. $psql1->query_safe(q(CREATE TEMP TABLE foo AS SELECT 42 AS val;)); +# Also create an empty table, so read path go straight through the +# extend-relation entry point. +$psql1->query_safe(q(CREATE TEMP TABLE empty_foo (val INT);)); + # Resolve the owner's temp schema so the probing session can refer to # the table by a fully-qualified name. my $tempschema = $node->safe_psql( @@ -67,6 +71,18 @@ qr/cannot access temporary tables of other sessions/, 'SELECT (seqscan via read_stream)'); +# INSERT into empty table goes through hio.c which calls RelationAddBlocks() to +# extend the table; that hits the check before new pages are created for the +# table. +$node->psql( + 'postgres', + "INSERT INTO $tempschema.empty_foo VALUES (42);", + stderr => \$stderr); +like( + $stderr, + qr/cannot access temporary tables of other sessions/, + 'INSERT (caught via hio.c)'); + # INSERT goes through hio.c which calls ReadBufferExtended() to find a # page with free space; that hits the existing check before any data # is written. From 558c4ea9a43b27d4ddb702fc455cf641d3792cde Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Fri, 3 Jul 2026 13:11:14 -0400 Subject: [PATCH 115/250] Use the proper comparator in gbt_bit_ssup_cmp. If we're dealing with leaf entries, the function to call is bitcmp not byteacmp. Using byteacmp didn't lead to any obvious failure, but it did result in sorting the entries in a way not matching the datatype's actual sort order. Hence the constructed index would be less efficient than one would expect, and in particular worse than what you got before this code was added in v18 (by commit e4309f73f). We might want to recommend that users reindex btree_gist indexes on bit/varbit columns. Author: Tom Lane Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/AH*AvQCYKhQGVvPWi1GiU4oY.8.1781609375063.Hmail.3020001251@tju.edu.cn Backpatch-through: 18 --- contrib/btree_gist/btree_bit.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/btree_gist/btree_bit.c b/contrib/btree_gist/btree_bit.c index 0df2ae20d8b..a8f2636c638 100644 --- a/contrib/btree_gist/btree_bit.c +++ b/contrib/btree_gist/btree_bit.c @@ -215,7 +215,7 @@ gbt_bit_ssup_cmp(Datum x, Datum y, SortSupport ssup) Datum result; /* for leaf items we expect lower == upper, so only compare lower */ - result = DirectFunctionCall2(byteacmp, + result = DirectFunctionCall2(bitcmp, PointerGetDatum(arg1.lower), PointerGetDatum(arg2.lower)); From 12c519207db01a9d4a6b47b205883928a083758f Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Fri, 3 Jul 2026 13:50:14 -0400 Subject: [PATCH 116/250] Fix btree_gist's NotEqual strategy on internal index pages. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gbt_var_consistent() handled the <> (BtreeGistNotEqual) strategy without distinguishing leaf from internal pages, unlike every other strategy. In particular, it tried to apply the datatype-specific f_eq method, which is completely wrong since internal keys might not have the same representation as leaf keys. This led to OOB reads and potentially crashes, and most likely to wrong query results as well. On leaf pages we can apply the inverse of what the Equal strategy does. On internal pages, use a correct implementation of what the previous code intended: we can descend if the query value equals both bounds, *so long as the bounds aren't truncated*. With truncated bounds we don't quite know the range of what's below, so we must always descend. Adjust the code in gbt_num_consistent() to look similar, too. This fixes a performance buglet in that there's no need to do two comparisons on a leaf entry, but the main point is just to keep code consistency. Reported-by: 王跃林 Author: Ayush Tiwari Reviewed-by: Tom Lane Discussion: https://postgr.es/m/AH*AvQCYKhQGVvPWi1GiU4oY.8.1781609375063.Hmail.3020001251@tju.edu.cn Backpatch-through: 14 --- contrib/btree_gist/btree_utils_num.c | 15 +++++++++++++-- contrib/btree_gist/btree_utils_var.c | 23 +++++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/contrib/btree_gist/btree_utils_num.c b/contrib/btree_gist/btree_utils_num.c index 346ee837d75..50b031d1e74 100644 --- a/contrib/btree_gist/btree_utils_num.c +++ b/contrib/btree_gist/btree_utils_num.c @@ -297,8 +297,19 @@ gbt_num_consistent(const GBT_NUMKEY_R *key, retval = tinfo->f_le(query, key->upper, flinfo); break; case BtreeGistNotEqualStrategyNumber: - retval = (!(tinfo->f_eq(query, key->lower, flinfo) && - tinfo->f_eq(query, key->upper, flinfo))); + if (is_leaf) + retval = !(tinfo->f_eq(query, key->lower, flinfo)); + else + { + /* + * If the upper/lower bounds are equal, then all entries below + * this node must have exactly that value. So we can avoid + * descending if the query equals both bounds. In all other + * cases, we must descend. + */ + retval = !(tinfo->f_eq(query, key->lower, flinfo) && + tinfo->f_eq(query, key->upper, flinfo)); + } break; default: retval = false; diff --git a/contrib/btree_gist/btree_utils_var.c b/contrib/btree_gist/btree_utils_var.c index 96cc08f9054..ae2191de37a 100644 --- a/contrib/btree_gist/btree_utils_var.c +++ b/contrib/btree_gist/btree_utils_var.c @@ -571,6 +571,13 @@ gbt_var_consistent(GBT_VARKEY_R *key, { bool retval = false; + /* + * Remember that f_cmp is for internal pages, f_eq etc for leaf pages, and + * on internal pages we need to check gbt_var_node_pf_match too. + * + * The leaf-page tests use swapped operands (e.g., f_gt(query, lower) + * means "lower < query"), which is why they look reversed. + */ switch (strategy) { case BTLessEqualStrategyNumber: @@ -611,8 +618,20 @@ gbt_var_consistent(GBT_VARKEY_R *key, || gbt_var_node_pf_match(key, query, tinfo); break; case BtreeGistNotEqualStrategyNumber: - retval = !(tinfo->f_eq(query, key->lower, collation, flinfo) && - tinfo->f_eq(query, key->upper, collation, flinfo)); + if (is_leaf) + retval = !(tinfo->f_eq(query, key->lower, collation, flinfo)); + else + { + /* + * If the upper/lower bounds are equal and not truncated, then + * all entries below this node must have exactly that value. + * So we can avoid descending if the query equals both bounds. + * In all other cases, we must descend. + */ + retval = tinfo->trnc || + !(tinfo->f_cmp(query, key->lower, collation, flinfo) == 0 && + tinfo->f_cmp(query, key->upper, collation, flinfo) == 0); + } break; default: retval = false; From a7f7958ab6bf368236334ce52e45be5e7502d2e7 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sat, 4 Jul 2026 11:34:26 -0400 Subject: [PATCH 117/250] Disallow renaming a rule to "_RETURN". ON SELECT rules must be named "_RETURN", while other kinds of rules must not be; this ancient restriction is depended on by various client code. We successfully enforced this convention in most places, but ALTER RULE allowed renaming a non-SELECT rule to "_RETURN". Notably, that would break dump/restore, since the eventual CREATE RULE command would reject the name. While at it, remove DefineQueryRewrite's hack to substitute "_RETURN" for the convention that was used before 7.3. We dropped other server-side code that supported restoring pre-7.3 dumps some time ago (notably in e58a59975 and nearby commits), but this bit was missed. Bug: #19543 Reported-by: Adam Pickering Author: Tom Lane Discussion: https://postgr.es/m/19543-461228e77f3b32fc@postgresql.org Backpatch-through: 14 --- src/backend/rewrite/rewriteDefine.c | 36 +++++++++++++---------------- src/test/regress/expected/rules.out | 2 ++ src/test/regress/sql/rules.sql | 1 + 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 8aa90b0d6fb..5dbe27f01a9 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -390,26 +390,11 @@ DefineQueryRewrite(const char *rulename, * ... and finally the rule must be named _RETURN. */ if (strcmp(rulename, ViewSelectRuleName) != 0) - { - /* - * In versions before 7.3, the expected name was _RETviewname. For - * backwards compatibility with old pg_dump output, accept that - * and silently change it to _RETURN. Since this is just a quick - * backwards-compatibility hack, limit the number of characters - * checked to a few less than NAMEDATALEN; this saves having to - * worry about where a multibyte character might have gotten - * truncated. - */ - if (strncmp(rulename, "_RET", 4) != 0 || - strncmp(rulename + 4, RelationGetRelationName(event_relation), - NAMEDATALEN - 4 - 4) != 0) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("view rule for \"%s\" must be named \"%s\"", - RelationGetRelationName(event_relation), - ViewSelectRuleName))); - rulename = pstrdup(ViewSelectRuleName); - } + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("view rule for \"%s\" must be named \"%s\"", + RelationGetRelationName(event_relation), + ViewSelectRuleName))); } else { @@ -844,6 +829,17 @@ RenameRewriteRule(RangeVar *relation, const char *oldName, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("renaming an ON SELECT rule is not allowed"))); + /* + * Conversely, if it's not an ON SELECT rule then it must *not* be named + * _RETURN. + */ + if (strcmp(newName, ViewSelectRuleName) == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("non-view rule for \"%s\" must not be named \"%s\"", + RelationGetRelationName(targetrel), + ViewSelectRuleName))); + /* OK, do the update */ namestrcpy(&(ruleform->rulename), newName); diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 6cf828ca8d0..23bcea5690c 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -3250,6 +3250,8 @@ ALTER RULE NewInsertRule ON rule_v1 RENAME TO "_RETURN"; -- already exists ERROR: rule "_RETURN" for relation "rule_v1" already exists ALTER RULE "_RETURN" ON rule_v1 RENAME TO abc; -- ON SELECT rule cannot be renamed ERROR: renaming an ON SELECT rule is not allowed +ALTER RULE rtest_t4_ins1 ON rtest_t4 RENAME TO "_RETURN"; -- also disallowed +ERROR: non-view rule for "rtest_t4" must not be named "_RETURN" DROP VIEW rule_v1; DROP TABLE rule_t1; -- diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql index fdd3ff1d161..4b6653981fb 100644 --- a/src/test/regress/sql/rules.sql +++ b/src/test/regress/sql/rules.sql @@ -1087,6 +1087,7 @@ SELECT * FROM rule_v1; ALTER RULE InsertRule ON rule_v1 RENAME TO NewInsertRule; -- doesn't exist ALTER RULE NewInsertRule ON rule_v1 RENAME TO "_RETURN"; -- already exists ALTER RULE "_RETURN" ON rule_v1 RENAME TO abc; -- ON SELECT rule cannot be renamed +ALTER RULE rtest_t4_ins1 ON rtest_t4 RENAME TO "_RETURN"; -- also disallowed DROP VIEW rule_v1; DROP TABLE rule_t1; From 1f8ab91c11ebf3d6521e2c9d67e321b1256f364c Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 6 Jul 2026 09:32:30 +0900 Subject: [PATCH 118/250] amcheck: Fix memory leak with gin_index_check() "prev_tuple" was overwritten with a new tuple coming from CopyIndexTuple() on each loop, leaking memory for every tuple processed on entry tree pages. The function uses a dedicated memory context, but this could leave unused large areas of memory while processing a large GIN index, the larger the worse. Oversight in 14ffaece0fb5. Author: Kirill Reshke Reviewed-by: Ewan Young Discussion: https://postgr.es/m/CALdSSPjTS6TYe5=5NfMUBYZyQu5cn=ABL6K5_OZjzGWqnwXeBw@mail.gmail.com Backpatch-through: 18 --- contrib/amcheck/verify_gin.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contrib/amcheck/verify_gin.c b/contrib/amcheck/verify_gin.c index c615d950736..0455ea2a2c1 100644 --- a/contrib/amcheck/verify_gin.c +++ b/contrib/amcheck/verify_gin.c @@ -638,6 +638,9 @@ gin_check_parent_keys_consistency(Relation rel, pfree(ipd); } + if (prev_tuple) + pfree(prev_tuple); + prev_tuple = CopyIndexTuple(idxtuple); prev_attnum = current_attnum; } From e2ad214dd80adbe905253b5689db4bf6e0ce7788 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Mon, 6 Jul 2026 09:46:15 +0900 Subject: [PATCH 119/250] Restore basebackup_progress_done() to preserve ABI Commit e7564ee8cdc, which fixed base backup progress reporting on backup failure, removed the external function basebackup_progress_done() because it was no longer used in core. When that change was backpatched to v15, it introduced an ABI break, which was reported by buildfarm member crake. This commit restores basebackup_progress_done() to preserve ABI compatibility, even though it is no longer used in core, rather than updating the .abi-compliance-history file. Because external backup tools may still call this function. Per buildfarm member crake. Reported-by: Andrew Dunstan Discussion: https://postgr.es/m/CAD5tBcJ+ktrEp=PT8Gq-f=8mA2cDtZMB-hDMV4mMJ+9V46qBeQ@mail.gmail.com Backpatch-through: 15-18 --- src/backend/backup/basebackup_progress.c | 12 ++++++++++++ src/include/backup/basebackup_sink.h | 1 + 2 files changed, 13 insertions(+) diff --git a/src/backend/backup/basebackup_progress.c b/src/backend/backup/basebackup_progress.c index e829d0626a4..c00d872b59b 100644 --- a/src/backend/backup/basebackup_progress.c +++ b/src/backend/backup/basebackup_progress.c @@ -242,3 +242,15 @@ basebackup_progress_transfer_wal(void) pgstat_progress_update_param(PROGRESS_BASEBACKUP_PHASE, PROGRESS_BASEBACKUP_PHASE_TRANSFER_WAL); } + +/* + * Advertise that we are no longer performing a backup. + * + * No longer used in core, but kept to preserve ABI compatibility in this + * branch. + */ +void +basebackup_progress_done(void) +{ + pgstat_progress_end_command(); +} diff --git a/src/include/backup/basebackup_sink.h b/src/include/backup/basebackup_sink.h index a298a79684d..8a5ee996a45 100644 --- a/src/include/backup/basebackup_sink.h +++ b/src/include/backup/basebackup_sink.h @@ -296,5 +296,6 @@ extern void basebackup_progress_wait_checkpoint(void); extern void basebackup_progress_estimate_backup_size(void); extern void basebackup_progress_wait_wal_archive(bbsink_state *); extern void basebackup_progress_transfer_wal(void); +extern void basebackup_progress_done(void); #endif From fe5d62951b3ccd382a530b790c3c2cb508b97c8c Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Mon, 6 Jul 2026 16:15:45 +0900 Subject: [PATCH 120/250] Fix qual pushdown past grouping with mismatched equivalence The planner has two optimizations that move a qual clause across a grouping boundary: subquery_planner transfers HAVING clauses to WHERE so they can be evaluated before aggregation, and qual_is_pushdown_safe pushes outer restriction clauses into a subquery past its DISTINCT, DISTINCT ON, window PARTITION BY, or set-operation grouping layer. Both produce wrong results when the moved clause's equivalence relation disagrees with the grouping's, since the clause then filters rows the grouping would have merged. The disagreement has two forms. A type may belong to multiple btree opfamilies whose equality operators disagree (e.g. record_ops vs record_image_ops); or the grouping may use a nondeterministic collation, where comparing the column under a different collation, or wrapping it in a function or operator, can distinguish values the collation considers equal. Because we cannot prove an arbitrary expression preserves that equality, a grouping column with a nondeterministic collation is safe to push only as a direct operand of a comparison under its own collation. Fix both call sites through a shared walker parameterized by a callback that maps each Var to the grouping equality operator for its column (or InvalidOid for non-grouping Vars). For HAVING, the callback recovers the SortGroupClause's eqop via the GROUP Var's varattno, which requires running before flatten_group_exprs while havingQual still contains GROUP Vars. For subquery pushdown, the callback recovers the eqop from subquery->distinctClause, a window's partitionClause, or any grouping node in the SetOperationStmt tree. The walker fires only when there is an equivalence boundary to cross, gated by either the existing UNSAFE_NOTIN_DISTINCTON_CLAUSE and UNSAFE_NOTIN_PARTITIONBY_CLAUSE flags or by a recursive check for any grouping node in the set-op tree. Back-patch to v18 only. The HAVING half relies on the RTE_GROUP mechanism introduced in v18 (commit 247dea89f), which is what lets us identify grouping expressions via GROUP Vars on pre-flatten havingQual. Pre-v18 branches lack that machinery, so a back-patch there would need a different approach. Given the absence of field reports of these bugs on back branches, the risk of carrying a different fix on stable branches is not justified. Author: Richard Guo Reviewed-by: Thom Brown Reviewed-by: Florin Irion Reviewed-by: Zsolt Parragi Reviewed-by: Tender Wang Reviewed-by: Chengpeng Yan Discussion: https://postgr.es/m/CAMbWs4-QLZpn3UVOpeG2fOxxhdnkDNMZ_3Zcm3dqJwRAphz68g@mail.gmail.com Backpatch-through: 18 --- src/backend/optimizer/path/allpaths.c | 179 +++++++++++ src/backend/optimizer/plan/planner.c | 261 +++++----------- src/backend/optimizer/util/clauses.c | 268 ++++++++++++++++ src/backend/utils/cache/lsyscache.c | 61 +++- src/include/optimizer/clauses.h | 14 + src/include/utils/lsyscache.h | 1 + src/test/regress/expected/aggregates.out | 80 ++++- .../regress/expected/collate.icu.utf8.out | 286 ++++++++++++++++-- src/test/regress/expected/subselect.out | 214 +++++++++++++ src/test/regress/sql/aggregates.sql | 46 ++- src/test/regress/sql/collate.icu.utf8.sql | 129 +++++++- src/test/regress/sql/subselect.sql | 105 +++++++ src/tools/pgindent/typedefs.list | 4 +- 13 files changed, 1409 insertions(+), 239 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 53208be5107..b53722036cd 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -154,6 +154,10 @@ static bool targetIsInAllPartitionLists(TargetEntry *tle, Query *query); static pushdown_safe_type qual_is_pushdown_safe(Query *subquery, Index rti, RestrictInfo *rinfo, pushdown_safety_info *safetyInfo); +static Oid pushdown_var_grouping_eqop(Var *var, void *context); +static Oid subquery_column_grouping_eqop(Query *subquery, AttrNumber attno); +static Oid setop_column_grouping_eqop(Node *setop, AttrNumber attno); +static bool setop_has_grouping(Node *setop); static void subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual); static void recurse_push_qual(Node *setOp, Query *topquery, @@ -3920,6 +3924,16 @@ targetIsInAllPartitionLists(TargetEntry *tle, Query *query) * * 5. rinfo's clause must not refer to any subquery output columns that were * found to be unsafe to reference by subquery_is_pushdown_safe(). + * + * 6. If the subquery has a grouping layer (DISTINCT, DISTINCT ON, window + * PARTITION BY, or a set operation that groups rows by equality), rinfo's + * clause must not apply a different equivalence relation to a grouping column + * than the grouping uses; otherwise it would distinguish rows the grouping + * considers equal, and pushing such a clause past the grouping would drop + * members of a group and change which row becomes the group's representative + * (or, for window functions, change per-partition values such as ranks and + * counts). See expression_has_grouping_conflict for the kinds of conflict + * detected. */ static pushdown_safe_type qual_is_pushdown_safe(Query *subquery, Index rti, RestrictInfo *rinfo, @@ -4016,9 +4030,174 @@ qual_is_pushdown_safe(Query *subquery, Index rti, RestrictInfo *rinfo, list_free(vars); + /* Check point 6 */ + if (safe == PUSHDOWN_SAFE && + (subquery->hasWindowFuncs || + subquery->distinctClause != NIL || + (subquery->setOperations != NULL && + setop_has_grouping(subquery->setOperations)))) + { + if (expression_has_grouping_conflict(qual, pushdown_var_grouping_eqop, + subquery)) + safe = PUSHDOWN_UNSAFE; + } + return safe; } +/* + * pushdown_var_grouping_eqop + * grouping_eqop_callback for qual_is_pushdown_safe. + * + * Returns the grouping equality operator for 'var' if it references a subquery + * output column that participates in the subquery's grouping layer; InvalidOid + * otherwise. + * + * 'context' is the subquery Query whose pushdown safety we're checking. + */ +static Oid +pushdown_var_grouping_eqop(Var *var, void *context) +{ + Query *subquery = (Query *) context; + Oid eqop; + + if (var->varlevelsup != 0) + return InvalidOid; + + eqop = subquery_column_grouping_eqop(subquery, var->varattno); + + /* + * qual_is_pushdown_safe ensures any level-0 subquery Var that reaches us + * references a grouping column. + */ + Assert(OidIsValid(eqop)); + + return eqop; +} + +/* + * subquery_column_grouping_eqop + * Return the equality operator that the subquery uses to group rows on + * the given output column, or InvalidOid if the column doesn't + * participate in any grouping mechanism. + * + * A subquery output column is grouping-relevant if it appears in + * subquery->distinctClause (covering both DISTINCT and DISTINCT ON), in every + * window's PARTITION BY clause, or is grouped by some node in a set-operation + * tree. In all of these cases the parser builds the SortGroupClause with the + * column's type-default equality operator via get_sort_group_operators, so any + * matching SortGroupClause carries the correct eqop. + */ +static Oid +subquery_column_grouping_eqop(Query *subquery, AttrNumber attno) +{ + TargetEntry *tle; + ListCell *lc; + + if (attno <= 0 || attno > list_length(subquery->targetList)) + return InvalidOid; + + tle = list_nth_node(TargetEntry, subquery->targetList, attno - 1); + + /* DISTINCT or DISTINCT ON */ + foreach(lc, subquery->distinctClause) + { + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc); + + if (sgc->tleSortGroupRef == tle->ressortgroupref) + return sgc->eqop; + } + + /* Window function PARTITION BY: must appear in every window's list. */ + if (subquery->hasWindowFuncs && subquery->windowClause != NIL) + { + Oid eqop = InvalidOid; + + foreach(lc, subquery->windowClause) + { + WindowClause *wc = (WindowClause *) lfirst(lc); + ListCell *lc2; + + foreach(lc2, wc->partitionClause) + { + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc2); + + if (sgc->tleSortGroupRef == tle->ressortgroupref) + break; + } + if (lc2 == NULL) + break; /* not present in this window's list */ + eqop = lfirst_node(SortGroupClause, lc2)->eqop; + } + if (lc == NULL) + return eqop; /* matched in every window */ + } + + /* Set operation */ + if (subquery->setOperations != NULL) + return setop_column_grouping_eqop(subquery->setOperations, attno); + + return InvalidOid; +} + +/* + * setop_column_grouping_eqop + * Recursively search a SetOperationStmt tree for any node that groups + * rows by equality, and return the equality operator used for the given + * output column. Returns InvalidOid if no node in the tree groups (i.e., + * an entirely-UNION-ALL tree). + * + * For any set operation other than UNION ALL, groupClauses is a positional + * list of SortGroupClauses, with element N-1 corresponding to output column N + * (see makeSortGroupClauseForSetOp). + */ +static Oid +setop_column_grouping_eqop(Node *setop, AttrNumber attno) +{ + SetOperationStmt *op; + Oid eqop; + + if (setop == NULL || !IsA(setop, SetOperationStmt)) + return InvalidOid; + + op = (SetOperationStmt *) setop; + + if (op->groupClauses != NIL && + attno >= 1 && attno <= list_length(op->groupClauses)) + { + SortGroupClause *sgc = list_nth_node(SortGroupClause, + op->groupClauses, attno - 1); + + return sgc->eqop; + } + + /* Recurse into children to find any inner grouping */ + eqop = setop_column_grouping_eqop(op->larg, attno); + if (OidIsValid(eqop)) + return eqop; + return setop_column_grouping_eqop(op->rarg, attno); +} + +/* + * setop_has_grouping + * Return true if any node in the SetOperationStmt tree groups rows by + * equality (i.e., has non-NIL groupClauses). + */ +static bool +setop_has_grouping(Node *setop) +{ + SetOperationStmt *op; + + if (setop == NULL || !IsA(setop, SetOperationStmt)) + return false; + + op = (SetOperationStmt *) setop; + if (op->groupClauses != NIL) + return true; + + return setop_has_grouping(op->larg) || setop_has_grouping(op->rarg); +} + /* * subquery_push_qual - push down a qual that we have determined is safe */ diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 55fddf804c4..cf49f8604fa 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -129,26 +129,21 @@ typedef struct } standard_qp_extra; /* - * Context for the find_having_collation_conflicts walker. - * - * ancestor_collids is a stack of inputcollids contributed by collation-aware - * ancestors of the current node. Entries are pushed before recursing into a - * node's children and popped afterwards, so the stack reflects exactly the - * inputcollids on the current root-to-node path. + * Context for find_having_conflicts. This is the callback context passed to + * expression_has_grouping_conflict in clauses.c. */ typedef struct { + Query *parse; Index group_rtindex; - List *ancestor_collids; -} having_collation_ctx; +} having_grouping_ctx; /* Local functions */ static Node *preprocess_expression(PlannerInfo *root, Node *expr, int kind); static void preprocess_qual_conditions(PlannerInfo *root, Node *jtnode); -static Bitmapset *find_having_collation_conflicts(Query *parse, - Index group_rtindex); -static bool having_collation_conflict_walker(Node *node, - having_collation_ctx *ctx); +static Bitmapset *find_having_conflicts(Query *parse, Index group_rtindex); +static Oid having_var_grouping_eqop(Var *var, void *context); +static Oid group_var_eqop(Query *parse, Var *var); static void grouping_planner(PlannerInfo *root, double tuple_fraction, SetOperationStmt *setops); static grouping_sets_data *preprocess_grouping_sets(PlannerInfo *root); @@ -673,7 +668,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, PlannerInfo *root; List *newWithCheckOptions; List *newHaving; - Bitmapset *havingCollationConflicts; + Bitmapset *havingPushdownConflicts; int havingIdx; bool hasOuterJoins; bool hasResultRTEs; @@ -1097,25 +1092,14 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, } /* - * Before we flatten GROUP Vars, check which HAVING clauses have collation - * conflicts. When GROUP BY uses a nondeterministic collation, values - * that are "equal" for grouping may be distinguishable under a different - * collation. If such a HAVING clause were moved to WHERE, it would - * filter individual rows before grouping, potentially eliminating some - * members of a group and thereby changing aggregate results. - * - * We do this check before flatten_group_exprs because we can easily - * identify grouping expressions by checking whether a Var references - * RTE_GROUP, and such Vars directly carry the GROUP BY collation as their - * varcollid. After flattening, these Vars are replaced by the underlying - * expressions, and we would have to match expressions in the HAVING - * clause back to grouping expressions, which is much more complex. + * Before we flatten GROUP Vars, identify HAVING clauses whose equality + * semantics disagree with the GROUP BY's. See find_having_conflicts. */ if (parse->hasGroupRTE) - havingCollationConflicts = - find_having_collation_conflicts(parse, root->group_rtindex); + havingPushdownConflicts = find_having_conflicts(parse, + root->group_rtindex); else - havingCollationConflicts = NULL; + havingPushdownConflicts = NULL; /* * Replace any Vars in the subquery's targetlist and havingQual that @@ -1161,13 +1145,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, * but it's okay: it's just an optimization to avoid running pull_varnos * when there cannot be any Vars in the HAVING clause.) * - * We also cannot do this if the HAVING clause uses a different collation - * than the GROUP BY for any grouping expression whose GROUP BY collation - * is nondeterministic. This is detected before flatten_group_exprs (see - * find_having_collation_conflicts above) and recorded in the - * havingCollationConflicts bitmapset. The bitmapset indexes remain valid - * here because flatten_group_exprs uses expression_tree_mutator, which - * preserves the list length and ordering of havingQual. + * We also cannot do this for HAVING clauses that conflict with GROUP BY + * on collation or operator family. Both kinds of conflict are detected + * before flatten_group_exprs (see find_having_conflicts above) and + * recorded in the havingPushdownConflicts bitmapset. The bitmapset + * indexes remain valid here because flatten_group_exprs uses + * expression_tree_mutator, which preserves the list length and ordering + * of havingQual. * * Also, it may be that the clause is so expensive to execute that we're * better off doing it only once per group, despite the loss of @@ -1209,7 +1193,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, if (contain_agg_clause(havingclause) || contain_volatile_functions(havingclause) || contain_subplans(havingclause) || - bms_is_member(havingIdx, havingCollationConflicts) || + bms_is_member(havingIdx, havingPushdownConflicts) || (parse->groupClause && parse->groupingSets && bms_is_member(root->group_rtindex, pull_varnos(root, havingclause)))) { @@ -1440,192 +1424,99 @@ preprocess_qual_conditions(PlannerInfo *root, Node *jtnode) } /* - * find_having_collation_conflicts - * Identify HAVING clauses that must not be moved to WHERE due to collation - * mismatches with GROUP BY. + * find_having_conflicts + * Identify HAVING clauses that must not be moved to WHERE because they + * apply a different equivalence relation than GROUP BY. Pushing such a + * clause to WHERE would filter individual rows before grouping happens, + * eliminating rows that GROUP BY would have merged into a single group + * and thereby changing aggregate results. + * + * The actual walking is done by expression_has_grouping_conflict; see that + * function for the kinds of conflict it looks for. We just iterate over + * havingQual and supply a HAVING-specific callback that identifies GROUP + * Vars. * * This must be called before flatten_group_exprs, while the HAVING clause * still contains GROUP Vars (Vars referencing RTE_GROUP). These GROUP Vars - * carry the GROUP BY collation as their varcollid. A GROUP Var with a - * nondeterministic varcollid conflicts whenever some collation-aware ancestor - * on its path applies a different inputcollid: that operator would distinguish - * values which the GROUP BY considers equal, so the clause is unsafe to push - * to WHERE. + * carry the GROUP BY collation as their varcollid and let us recover the + * grouping eqop via varattno. After flattening, those Vars are replaced by + * the underlying expressions, and matching back to grouping expressions is + * much harder. * * Returns a Bitmapset of zero-based indexes into the havingQual list for - * clauses that have collation conflicts and must stay in HAVING. + * clauses that conflict and must stay in HAVING. */ static Bitmapset * -find_having_collation_conflicts(Query *parse, Index group_rtindex) +find_having_conflicts(Query *parse, Index group_rtindex) { Bitmapset *result = NULL; - having_collation_ctx ctx; + having_grouping_ctx ctx; int idx; if (parse->havingQual == NULL) return NULL; + ctx.parse = parse; ctx.group_rtindex = group_rtindex; - ctx.ancestor_collids = NIL; idx = 0; foreach_ptr(Node, clause, (List *) parse->havingQual) { - if (having_collation_conflict_walker(clause, &ctx)) + if (expression_has_grouping_conflict(clause, having_var_grouping_eqop, + &ctx)) result = bms_add_member(result, idx); idx++; - Assert(ctx.ancestor_collids == NIL); } return result; } /* - * Walker function for find_having_collation_conflicts. + * having_var_grouping_eqop + * grouping_eqop_callback for find_having_conflicts. * - * Walk the clause top-down, maintaining a stack of inputcollids contributed - * by collation-aware ancestors. At each GROUP Var with a nondeterministic - * varcollid, the clause has a conflict if any ancestor's inputcollid differs - * from the GROUP Var's varcollid. Most collation-aware nodes expose their - * inputcollid through exprInputCollation(). Two structural exceptions need - * special handling: - * - * - RowCompareExpr carries one inputcollid per column in inputcollids[], so we - * descend into its (largs[i], rargs[i]) pairs explicitly with the matching - * collation pushed onto the stack. - * - * - A simple CASE (CaseExpr with a non-NULL arg) holds the arg outside the - * WHEN's OpExpr, even though the WHEN's OpExpr is the place where the - * comparison's inputcollid lives. Parse analysis builds each WHEN as - * "OpExpr(CaseTestExpr op val)" -- the CaseTestExpr is a placeholder for - * the arg. Before walking cexpr->arg we therefore push every WHEN's - * inputcollid onto the ancestor stack, so a GROUP Var at the arg is - * checked against the same collations the WHEN comparisons would apply. - * The WHEN bodies and defresult are then walked under the unchanged stack - * so their own collation contexts are picked up by the default path. + * Returns the GROUP BY equality operator for 'var' if it references the + * query's RTE_GROUP, or InvalidOid otherwise. */ -static bool -having_collation_conflict_walker(Node *node, having_collation_ctx *ctx) +static Oid +having_var_grouping_eqop(Var *var, void *context) { - Oid this_collid; - bool result; + having_grouping_ctx *ctx = (having_grouping_ctx *) context; - if (node == NULL) - return false; - - if (IsA(node, Var)) - { - Var *var = (Var *) node; - - /* We should not see any upper-level Vars here */ - Assert(var->varlevelsup == 0); + if (var->varno != ctx->group_rtindex || var->varlevelsup != 0) + return InvalidOid; - if (var->varno == ctx->group_rtindex && - OidIsValid(var->varcollid) && - !get_collation_isdeterministic(var->varcollid)) - { - foreach_oid(collid, ctx->ancestor_collids) - { - if (collid != var->varcollid) - return true; - } - } - return false; - } - - if (IsA(node, RowCompareExpr)) - { - RowCompareExpr *rcexpr = (RowCompareExpr *) node; - ListCell *lc_l; - ListCell *lc_r; - ListCell *lc_c; - - /* - * Each column of a row comparison is compared under its own - * inputcollids[i]. Walk each (largs[i], rargs[i]) pair with that - * collation pushed, so a Var in column i is checked against the - * collation that actually applies to it. - */ - forthree(lc_l, rcexpr->largs, - lc_r, rcexpr->rargs, - lc_c, rcexpr->inputcollids) - { - Oid collid = lfirst_oid(lc_c); - bool found; - - if (OidIsValid(collid)) - ctx->ancestor_collids = lappend_oid(ctx->ancestor_collids, - collid); - - found = having_collation_conflict_walker((Node *) lfirst(lc_l), - ctx) || - having_collation_conflict_walker((Node *) lfirst(lc_r), - ctx); + return group_var_eqop(ctx->parse, var); +} - if (OidIsValid(collid)) - ctx->ancestor_collids = - list_delete_last(ctx->ancestor_collids); +/* + * group_var_eqop + * Return the equality operator that GROUP BY uses for the given GROUP Var. + * + * A GROUP Var's varattno is its 1-based position in the RTE_GROUP's groupexprs + * list, which addRangeTableEntryForGroup built by iterating parse->groupClause + * and including every SortGroupClause whose TLE was present in the targetlist. + * Replay that traversal here to recover the SortGroupClause for the given + * varattno. + */ +static Oid +group_var_eqop(Query *parse, Var *var) +{ + int counter = 0; - if (found) - return true; - } - return false; - } + Assert(var->varlevelsup == 0); - if (IsA(node, CaseExpr) && ((CaseExpr *) node)->arg != NULL) + foreach_node(SortGroupClause, sgc, parse->groupClause) { - CaseExpr *cexpr = (CaseExpr *) node; - int saved_len = list_length(ctx->ancestor_collids); - bool found; - - /* - * Push every WHEN's inputcollid before walking cexpr->arg, since each - * WHEN implicitly compares the arg under that inputcollid. - */ - foreach_node(CaseWhen, cw, cexpr->args) - { - Oid collid = exprInputCollation((Node *) cw->expr); - - if (OidIsValid(collid)) - ctx->ancestor_collids = lappend_oid(ctx->ancestor_collids, - collid); - } - - found = having_collation_conflict_walker((Node *) cexpr->arg, ctx); - - ctx->ancestor_collids = list_truncate(ctx->ancestor_collids, - saved_len); - - if (found) - return true; - - /* - * Walk the WHEN bodies and defresult under the unchanged ancestor - * stack; any inputcollids inside them are picked up by the default - * path. - */ - foreach_node(CaseWhen, cw, cexpr->args) - { - if (having_collation_conflict_walker((Node *) cw->expr, ctx) || - having_collation_conflict_walker((Node *) cw->result, ctx)) - return true; - } - return having_collation_conflict_walker((Node *) cexpr->defresult, - ctx); + if (get_sortgroupclause_tle(sgc, parse->targetList) == NULL) + continue; + if (++counter == var->varattno) + return sgc->eqop; } - this_collid = exprInputCollation(node); - if (OidIsValid(this_collid)) - ctx->ancestor_collids = lappend_oid(ctx->ancestor_collids, - this_collid); - - result = expression_tree_walker(node, having_collation_conflict_walker, - ctx); - - if (OidIsValid(this_collid)) - ctx->ancestor_collids = list_delete_last(ctx->ancestor_collids); - - return result; + elog(ERROR, "could not find GROUP clause for GROUP Var attno %d", + var->varattno); + return InvalidOid; /* keep compiler quiet */ } /* diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 3a2d753907b..1d341db7d06 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -94,6 +94,17 @@ typedef struct List *safe_param_ids; /* PARAM_EXEC Param IDs to treat as safe */ } max_parallel_hazard_context; +/* + * Walker context for expression_has_grouping_conflict. get_eqop is a callback + * that returns the equality operator used for grouping. cb_context is opaque + * to the walker and is forwarded to get_eqop unchanged. + */ +typedef struct +{ + grouping_eqop_callback get_eqop; + void *cb_context; +} grouping_walker_ctx; + static bool contain_agg_clause_walker(Node *node, void *context); static bool find_window_functions_walker(Node *node, WindowFuncLists *lists); static bool contain_subplans_walker(Node *node, void *context); @@ -111,6 +122,11 @@ static Relids find_nonnullable_rels_walker(Node *node, bool top_level); static List *find_nonnullable_vars_walker(Node *node, bool top_level); static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK); static bool convert_saop_to_hashed_saop_walker(Node *node, void *context); +static bool grouping_conflict_walker(Node *node, grouping_walker_ctx *ctx); +static bool grouping_check_operands(Oid opno, Oid inputcollid, + List *args, grouping_walker_ctx *ctx); +static bool grouping_check_operand(Node *arg, Oid opno, Oid inputcollid, + grouping_walker_ctx *ctx); static Node *eval_const_expressions_mutator(Node *node, eval_const_expressions_context *context); static bool contain_non_const_walker(Node *node, void *context); @@ -5444,6 +5460,258 @@ pull_paramids_walker(Node *node, Bitmapset **context) return expression_tree_walker(node, pull_paramids_walker, context); } +/* + * expression_has_grouping_conflict + * Detect whether 'expr' would distinguish rows that a grouping mechanism + * (GROUP BY, DISTINCT, DISTINCT ON, window PARTITION BY, or set operation) + * considers equal. + * + * The caller supplies a get_eqop callback (see clauses.h) so the same walker + * serves every grouping context. The callback identifies a grouping column by + * returning a valid eqop for its Var. A grouping column is safe to reference + * only if the reference yields the same result for every value the grouping + * treats as equal. Otherwise, pushing the clause past the grouping could + * discard rows that the grouping would have combined into a single group. + * + * The reference is provably safe only when the grouping column is a direct + * operand of a comparison that tests the grouping's own equality. Such an + * operand is rejected when the comparison's operator does not have equality + * semantics compatible with the grouping eqop, or, for a nondeterministic + * collation, when the comparison applies a collation other than the column's. + * + * For a nondeterministic collation, every other reference is rejected: a + * comparison under a different collation, and any function or operator over + * the column, because we cannot tell whether the function yields the same + * result for values the grouping treats as equal, and many do not. A column + * with a deterministic collation is not restricted this way. + * + * This leaves one case uncaught: with a deterministic collation, a function + * over the column can still feed a finer comparison than the direct-operand + * check sees, for example record_image_ops over a rebuilt record, or scale() + * over numeric where two equal values differ in scale. Catching it would + * require knowing that a type's equality is bitwise, which we do not test + * here. + * + * Returns true if any such conflict exists. + */ +bool +expression_has_grouping_conflict(Node *expr, + grouping_eqop_callback get_eqop, + void *context) +{ + grouping_walker_ctx ctx; + + if (expr == NULL) + return false; + + ctx.get_eqop = get_eqop; + ctx.cb_context = context; + + return grouping_conflict_walker(expr, &ctx); +} + +/* + * Walker function for expression_has_grouping_conflict. + * + * A comparison node checks its direct operands with grouping_check_operand, + * which does not recurse into a grouping-column operand. A grouping column + * therefore reaches the Var branch only when it is referenced in some other + * way: wrapped in a function or other expression, used as the whole qual (a + * bare boolean column), or used as an operand of an operator that is not a + * btree/hash member and so is not treated as a comparison here. + * + * Comparison nodes are OpExpr/ScalarArrayOpExpr whose operator is a btree/hash + * member, and RowCompareExpr (one operator and collation per column). A + * simple CASE (CaseExpr with a non-NULL arg) is a comparison in disguise: + * parse analysis builds each WHEN as "OpExpr(CaseTestExpr op val)", with the + * CaseTestExpr standing in for the arg, so the arg is effectively an operand + * of each WHEN's comparison. Those WHEN operators are always the type-default + * "=", matching the grouping eqop, so only a collation conflict is possible + * there. + */ +static bool +grouping_conflict_walker(Node *node, grouping_walker_ctx *ctx) +{ + if (node == NULL) + return false; + + if (IsA(node, Var)) + { + Var *var = (Var *) node; + + /* + * A grouping column reaches here when it was not handled as a direct + * operand by a comparison node above (see the function header). That + * is safe for a deterministic collation, but not for a + * nondeterministic one, where the reference may distinguish values + * the grouping considers equal. A bare boolean qual is safe too: + * boolean is not collatable, so it takes the deterministic path here. + */ + if (OidIsValid(ctx->get_eqop(var, ctx->cb_context)) && + OidIsValid(var->varcollid) && + !get_collation_isdeterministic(var->varcollid)) + return true; + return false; + } + else if (IsA(node, OpExpr)) + { + OpExpr *opexpr = (OpExpr *) node; + + if (op_is_safe_index_member(opexpr->opno)) + return grouping_check_operands(opexpr->opno, opexpr->inputcollid, + opexpr->args, ctx); + /* fall through */ + } + else if (IsA(node, ScalarArrayOpExpr)) + { + ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) node; + + if (op_is_safe_index_member(saop->opno)) + return grouping_check_operands(saop->opno, saop->inputcollid, + saop->args, ctx); + /* fall through */ + } + else if (IsA(node, RowCompareExpr)) + { + RowCompareExpr *rcexpr = (RowCompareExpr *) node; + ListCell *lc_l; + ListCell *lc_r; + ListCell *lc_o; + ListCell *lc_c; + + /* Each column is compared under its own operator and inputcollid. */ + forfour(lc_l, rcexpr->largs, + lc_r, rcexpr->rargs, + lc_o, rcexpr->opnos, + lc_c, rcexpr->inputcollids) + { + Oid opno = lfirst_oid(lc_o); + Oid collid = lfirst_oid(lc_c); + + if (grouping_check_operand((Node *) lfirst(lc_l), opno, collid, ctx) || + grouping_check_operand((Node *) lfirst(lc_r), opno, collid, ctx)) + return true; + } + return false; + } + else if (IsA(node, CaseExpr) && ((CaseExpr *) node)->arg != NULL) + { + CaseExpr *cexpr = (CaseExpr *) node; + Node *arg = (Node *) cexpr->arg; + + /* Look through RelabelType to find a direct Var arg. */ + while (arg && IsA(arg, RelabelType)) + arg = (Node *) ((RelabelType *) arg)->arg; + + if (arg && IsA(arg, Var)) + { + Var *var = (Var *) arg; + + /* + * The arg is a grouping column compared by every WHEN. For a + * nondeterministic collation, reject if any WHEN applies a + * different collation. + */ + if (OidIsValid(ctx->get_eqop(var, ctx->cb_context)) && + OidIsValid(var->varcollid) && + !get_collation_isdeterministic(var->varcollid)) + { + foreach_node(CaseWhen, cw, cexpr->args) + { + Oid collid = exprInputCollation((Node *) cw->expr); + + if (OidIsValid(collid) && collid != var->varcollid) + return true; + } + } + } + else if (grouping_conflict_walker((Node *) cexpr->arg, ctx)) + { + /* arg is a complex expression; walked as a non-operand */ + return true; + } + + /* + * Walk the WHEN conditions, their results, and the default result as + * non-operands. The WHEN conditions hold a CaseTestExpr in place of + * the arg, so they contribute no grouping operand of their own, but + * the condition expression or the substitution result may reference + * another grouping column. + */ + foreach_node(CaseWhen, cw, cexpr->args) + { + if (grouping_conflict_walker((Node *) cw->expr, ctx) || + grouping_conflict_walker((Node *) cw->result, ctx)) + return true; + } + return grouping_conflict_walker((Node *) cexpr->defresult, ctx); + } + + return expression_tree_walker(node, grouping_conflict_walker, ctx); +} + +/* + * grouping_check_operands + * Check every argument of a comparison node as a direct operand of the + * comparison's operator 'opno' and collation 'inputcollid'. + */ +static bool +grouping_check_operands(Oid opno, Oid inputcollid, List *args, + grouping_walker_ctx *ctx) +{ + ListCell *lc; + + foreach(lc, args) + { + if (grouping_check_operand((Node *) lfirst(lc), opno, inputcollid, ctx)) + return true; + } + return false; +} + +/* + * grouping_check_operand + * Handle one operand 'arg' of a comparison with operator 'opno' and + * collation 'inputcollid'. + * + * If 'arg' is a grouping column (after looking through RelabelType), verify + * that comparison's operator has equality semantics compatible with the + * grouping eqop and, for a nondeterministic collation, that it uses the same + * collation; such a direct operand is then fully handled and is not recursed + * into. Any other operand is walked normally, so a grouping column buried + * inside it is seen as a non-operand reference. + */ +static bool +grouping_check_operand(Node *arg, Oid opno, Oid inputcollid, + grouping_walker_ctx *ctx) +{ + Node *node = arg; + + while (node && IsA(node, RelabelType)) + node = (Node *) ((RelabelType *) node)->arg; + + if (node && IsA(node, Var)) + { + Var *var = (Var *) node; + Oid grouping_eqop = ctx->get_eqop(var, ctx->cb_context); + + if (OidIsValid(grouping_eqop)) + { + /* incompatible equality semantics */ + if (!equality_ops_are_compatible(opno, grouping_eqop)) + return true; + /* nondeterministic collation compared under a different collation */ + if (OidIsValid(var->varcollid) && + !get_collation_isdeterministic(var->varcollid) && + inputcollid != var->varcollid) + return true; + } + return false; /* direct operand handled; do not recurse */ + } + + return grouping_conflict_walker(arg, ctx); +} + /* * Build ScalarArrayOpExpr on top of 'exprs.' 'haveNonConst' indicates * whether at least one of the expressions is not Const. When it's false, diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 366031e96c9..11ccac505db 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -826,15 +826,22 @@ get_op_index_interpretation(Oid opno) /* * equality_ops_are_compatible - * Return true if the two given equality operators have compatible + * Return true if the two given operators have compatible equality * semantics. * * This is trivially true if they are the same operator. Otherwise, - * Otherwise, we look to see if they both belong to an opfamily that - * guarantees compatible semantics for equality. Either finding allows us to - * assume that they have compatible notions of equality. (The reason we need - * to do these pushups is that one might be a cross-type operator; for - * instance int24eq vs int4eq.) + * we look to see if they both belong to an opfamily that guarantees + * compatible semantics for equality. Either finding allows us to assume + * that they have compatible notions of equality. + * + * The typical use is to compare two equality operators (for instance the + * cross-type operators int24eq vs int4eq), but the test is meaningful for + * any pair of operators in a btree/hash opfamily. Btree marks its + * opfamilies as amconsistentequality, which guarantees that every member + * of the family (=, <, <=, >, >=) agrees on the equivalence relation + * defined by the family's "=". So a non-equality operator and an + * equality operator from the same opfamily are also "compatible" in this + * sense. */ bool equality_ops_are_compatible(Oid opno1, Oid opno2) @@ -973,6 +980,48 @@ collations_agree_on_equality(Oid coll1, Oid coll2) return true; } +/* + * op_is_safe_index_member + * Check if the operator is a member of a B-tree or Hash operator family. + * + * Membership in such an opfamily has several useful implications: the operator + * returns non-null for non-null inputs (i.e. "null-safety", required so that + * the operator doesn't break index integrity), and it agrees with other + * members of the same opfamily on equality semantics. Callers use this check + * as a proxy for any of those properties. + */ +bool +op_is_safe_index_member(Oid opno) +{ + bool result = false; + CatCList *catlist; + int i; + + /* + * Search pg_amop to see if the target operator is registered for any + * btree or hash opfamily. + */ + catlist = SearchSysCacheList1(AMOPOPID, ObjectIdGetDatum(opno)); + + for (i = 0; i < catlist->n_members; i++) + { + HeapTuple tuple = &catlist->members[i]->tuple; + Form_pg_amop aform = (Form_pg_amop) GETSTRUCT(tuple); + + /* Check if the AM is B-tree or Hash */ + if (aform->amopmethod == BTREE_AM_OID || + aform->amopmethod == HASH_AM_OID) + { + result = true; + break; + } + } + + ReleaseSysCacheList(catlist); + + return result; +} + /* ---------- AMPROC CACHES ---------- */ diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h index 0dffec00ede..5935c50ffe1 100644 --- a/src/include/optimizer/clauses.h +++ b/src/include/optimizer/clauses.h @@ -23,6 +23,16 @@ typedef struct List **windowFuncs; /* lists of WindowFuncs for each winref */ } WindowFuncLists; +/* + * Callback used by expression_has_grouping_conflict below. Given a Var, the + * callback returns the equality operator that the relevant grouping mechanism + * (GROUP BY, DISTINCT, DISTINCT ON, window PARTITION BY, or set operation) + * uses for the column the Var references, or InvalidOid if the Var does not + * participate in that grouping. Returning InvalidOid signals "not a grouping + * column" to both the opfamily and collation checks. + */ +typedef Oid (*grouping_eqop_callback) (Var *var, void *context); + extern bool contain_agg_clause(Node *clause); extern bool contain_window_function(Node *clause); @@ -55,4 +65,8 @@ extern Query *inline_set_returning_function(PlannerInfo *root, extern Bitmapset *pull_paramids(Expr *expr); +extern bool expression_has_grouping_conflict(Node *expr, + grouping_eqop_callback get_eqop, + void *context); + #endif /* CLAUSES_H */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 81b152da421..e0c2fc7f7aa 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -92,6 +92,7 @@ extern List *get_op_index_interpretation(Oid opno); extern bool equality_ops_are_compatible(Oid opno1, Oid opno2); extern bool comparison_ops_are_compatible(Oid opno1, Oid opno2); extern bool collations_agree_on_equality(Oid coll1, Oid coll2); +extern bool op_is_safe_index_member(Oid opno); extern Oid get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype, int16 procnum); extern char *get_attname(Oid relid, AttrNumber attnum, bool missing_ok); diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index cdb4ecf288f..e2a8e19f7b6 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -1515,17 +1515,26 @@ explain (costs off) select y,z from t2 group by y,z; -> Seq Scan on t2 (3 rows) +drop table t1 cascade; +NOTICE: drop cascades to table t1c +drop table t2; +drop table t3; +drop table p_t1; +-- A composite type used by the tests below to exercise the asymmetry +-- between record_ops (per-field equality, the default) and record_image_ops +-- (bytewise equality): values like row(1.0) and row(1.00) are field-equal +-- but byte-distinct. +create type avg_rec as (x numeric); -- A unique index proves uniqueness only under its own opfamily. When the -- GROUP BY's eqop comes from a different opfamily with looser equality, -- rows the index regards as distinct can collapse into one GROUP BY group, -- so the index is not usable for removing redundant columns. -create type t_rec as (x numeric); -create temp table t_opf (a t_rec not null, b text); +create temp table t_opf (a avg_rec not null, b text); create unique index on t_opf (a record_image_ops); -- (1.0) and (1.00) are bytewise distinct but logically equal as records; -- the index admits both, but GROUP BY a (default record_ops) would merge -- them, so b must be retained as a grouping key. -insert into t_opf values (row(1.0)::t_rec, 'X'), (row(1.00)::t_rec, 'Y'); +insert into t_opf values (row(1.0)::avg_rec, 'X'), (row(1.00)::avg_rec, 'Y'); explain (costs off) select a, b from t_opf group by a, b order by b; QUERY PLAN @@ -1545,12 +1554,65 @@ select a, b from t_opf group by a, b order by b; (2 rows) drop table t_opf; -drop type t_rec; -drop table t1 cascade; -NOTICE: drop cascades to table t1c -drop table t2; -drop table t3; -drop table p_t1; +-- A HAVING clause that uses an equality operator from a different opfamily +-- than the GROUP BY's eqop must NOT be pushed down to WHERE. +create temp table t_having (id int, a avg_rec); +insert into t_having values + (1, row(1.0)::avg_rec), + (2, row(1.00)::avg_rec), + (3, row(2)::avg_rec); +-- the clause must stay in HAVING +explain (costs off) +select a, count(*) from t_having group by a having a *= row(1.0)::avg_rec; + QUERY PLAN +----------------------------------- + HashAggregate + Group Key: a + Filter: (a *= '(1.0)'::avg_rec) + -> Seq Scan on t_having +(4 rows) + +select a, count(*) from t_having group by a having a *= row(1.0)::avg_rec; + a | count +-------+------- + (1.0) | 2 +(1 row) + +-- the clause must stay in HAVING +explain (costs off) +select a, count(*) from t_having group by a having a *= any (array[row(1.0)::avg_rec]); + QUERY PLAN +--------------------------------------------- + HashAggregate + Group Key: a + Filter: (a *= ANY ('{(1.0)}'::avg_rec[])) + -> Seq Scan on t_having +(4 rows) + +select a, count(*) from t_having group by a having a *= any (array[row(1.0)::avg_rec]); + a | count +-------+------- + (1.0) | 2 +(1 row) + +-- the clause can be pushed down to WHERE +explain (costs off) +select a, count(*) from t_having group by a having a = row(1.0)::avg_rec; + QUERY PLAN +---------------------------------------- + GroupAggregate + -> Seq Scan on t_having + Filter: (a = '(1.0)'::avg_rec) +(3 rows) + +select a, count(*) from t_having group by a having a = row(1.0)::avg_rec; + a | count +-------+------- + (1.0) | 2 +(1 row) + +drop table t_having; +drop type avg_rec; -- -- Test GROUP BY matching of join columns that are type-coerced due to USING -- diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index 66093040965..ba247282476 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -2112,7 +2112,7 @@ SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensi abc | 2 (1 row) --- Negative: function applied to grouped column with conflicting collation +-- Negative: function over the grouping column, conflicting collation EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x) = 'ABC' COLLATE case_sensitive; QUERY PLAN @@ -2129,18 +2129,36 @@ SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x) = 'ABC' COLLATE case_ abc | 2 (1 row) --- Positive: function with same collation as GROUP BY +-- Negative: function over the grouping column whose result is compared as an +-- integer, under no collation +EXPLAIN (COSTS OFF) +SELECT x, count(*) FROM test3ci GROUP BY x HAVING ascii(x) = 97; + QUERY PLAN +--------------------------- + HashAggregate + Group Key: x + Filter: (ascii(x) = 97) + -> Seq Scan on test3ci +(4 rows) + +SELECT x, count(*) FROM test3ci GROUP BY x HAVING ascii(x) = 97; + x | count +-----+------- + abc | 2 +(1 row) + +-- Negative: a function wrapping the grouping column is not provably safe even +-- when compared under the matching collation, since the function need not +-- preserve the collation's equality EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x) = 'ABC' COLLATE case_insensitive; - QUERY PLAN -------------------------------------------------------------------------- - GroupAggregate + QUERY PLAN +------------------------------------------------------------- + HashAggregate Group Key: x - -> Sort - Sort Key: x COLLATE case_insensitive - -> Seq Scan on test3ci - Filter: (upper(x) = 'ABC'::text COLLATE case_insensitive) -(6 rows) + Filter: (upper(x) = 'ABC'::text COLLATE case_insensitive) + -> Seq Scan on test3ci +(4 rows) SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x) = 'ABC' COLLATE case_insensitive; x | count @@ -2148,8 +2166,8 @@ SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x) = 'ABC' COLLATE case_ abc | 2 (1 row) --- Negative: inner function has conflicting collation, even though outer --- operator's collation matches GROUP BY due to a COLLATE override +-- Negative: same, with the grouping column wrapped in a function whose input +-- collation is overridden; still not a direct operand, so it stays in HAVING EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x COLLATE case_sensitive) COLLATE case_insensitive = 'ABC'; QUERY PLAN @@ -2168,17 +2186,17 @@ SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x COLLATE case_sensitive -- Mixed AND: conflicting clause stays in HAVING, safe clause pushed to WHERE EXPLAIN (COSTS OFF) -SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_sensitive AND length(x) > 1; - QUERY PLAN ----------------------------------------------------- +SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_sensitive AND x >= 'a' COLLATE case_insensitive; + QUERY PLAN +----------------------------------------------------------- HashAggregate Group Key: x Filter: (x = 'abc'::text COLLATE case_sensitive) -> Seq Scan on test3ci - Filter: (length(x) > 1) + Filter: (x >= 'a'::text COLLATE case_insensitive) (5 rows) -SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_sensitive AND length(x) > 1; +SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_sensitive AND x >= 'a' COLLATE case_insensitive; x | count -----+------- abc | 2 @@ -2186,15 +2204,15 @@ SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_sensiti -- Positive: AND of two safe clauses, both can be pushed EXPLAIN (COSTS OFF) -SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive AND length(x) > 1; - QUERY PLAN ----------------------------------------------------------------------------------- +SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive AND x >= 'a' COLLATE case_insensitive; + QUERY PLAN +------------------------------------------------------------------------------------------------------------ GroupAggregate -> Seq Scan on test3ci - Filter: ((x = 'abc'::text COLLATE case_insensitive) AND (length(x) > 1)) + Filter: ((x >= 'a'::text COLLATE case_insensitive) AND (x = 'abc'::text COLLATE case_insensitive)) (3 rows) -SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive AND length(x) > 1; +SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive AND x >= 'a' COLLATE case_insensitive; x | count -----+------- abc | 2 @@ -2338,6 +2356,230 @@ SELECT x, count(*) FROM test3cs GROUP BY x HAVING x = 'abc' COLLATE case_insensi ABC | 1 (2 rows) +-- Test WHERE-pushdown past a grouping layer (DISTINCT, DISTINCT ON, window +-- PARTITION BY) when the qual applies a different collation than the +-- grouping column's nondeterministic collation. The qual would distinguish +-- rows the grouping considers equal, so it must NOT be pushed inside the +-- subquery. +CREATE TABLE pushdown_ci (id int, x text COLLATE case_insensitive); +INSERT INTO pushdown_ci VALUES (1, 'ABC'), (2, 'abc'), (3, 'def'); +-- DISTINCT ON: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT ON (x) id, x FROM pushdown_ci ORDER BY x, id) s +WHERE x = 'abc' COLLATE case_sensitive; + QUERY PLAN +-------------------------------------------------------------------------------- + Subquery Scan on s + Filter: (s.x = 'abc'::text COLLATE case_sensitive) + -> Unique + -> Sort + Sort Key: pushdown_ci.x COLLATE case_insensitive, pushdown_ci.id + -> Seq Scan on pushdown_ci +(6 rows) + +SELECT * FROM (SELECT DISTINCT ON (x) id, x FROM pushdown_ci ORDER BY x, id) s +WHERE x = 'abc' COLLATE case_sensitive; + id | x +----+--- +(0 rows) + +-- Window function PARTITION BY: conflict, qual stays outside the WindowAgg +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT id, x, count(*) OVER (PARTITION BY x) AS cnt FROM pushdown_ci +) s +WHERE x = 'abc' COLLATE case_sensitive; + QUERY PLAN +---------------------------------------------------------------- + Subquery Scan on s + Filter: (s.x = 'abc'::text COLLATE case_sensitive) + -> WindowAgg + Window: w1 AS (PARTITION BY pushdown_ci.x) + -> Sort + Sort Key: pushdown_ci.x COLLATE case_insensitive + -> Seq Scan on pushdown_ci +(7 rows) + +SELECT * FROM ( + SELECT id, x, count(*) OVER (PARTITION BY x) AS cnt FROM pushdown_ci +) s +WHERE x = 'abc' COLLATE case_sensitive; + id | x | cnt +----+-----+----- + 2 | abc | 2 +(1 row) + +-- Plain DISTINCT: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT x FROM pushdown_ci) s +WHERE x = 'abc' COLLATE case_sensitive; + QUERY PLAN +------------------------------------------------------ + Subquery Scan on s + Filter: (s.x = 'abc'::text COLLATE case_sensitive) + -> HashAggregate + Group Key: pushdown_ci.x + -> Seq Scan on pushdown_ci +(5 rows) + +SELECT * FROM (SELECT DISTINCT x FROM pushdown_ci) s +WHERE x = 'abc' COLLATE case_sensitive; + x +--- +(0 rows) + +-- Positive: matching collation, safe to push past the grouping +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT ON (x) id, x FROM pushdown_ci ORDER BY x, id) s +WHERE x = 'abc' COLLATE case_insensitive; + QUERY PLAN +------------------------------------------------------------------ + Limit + -> Sort + Sort Key: pushdown_ci.id + -> Seq Scan on pushdown_ci + Filter: (x = 'abc'::text COLLATE case_insensitive) +(5 rows) + +SELECT * FROM (SELECT DISTINCT ON (x) id, x FROM pushdown_ci ORDER BY x, id) s +WHERE x = 'abc' COLLATE case_insensitive; + id | x +----+----- + 1 | ABC +(1 row) + +-- Set operations: any operation other than UNION ALL groups rows by equality, +-- so the same collation-mismatch rules apply. +CREATE TABLE pushdown_ci2 (x text COLLATE case_insensitive); +INSERT INTO pushdown_ci2 VALUES ('abc'); +-- UNION: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT x FROM pushdown_ci UNION SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + QUERY PLAN +------------------------------------------------------ + Subquery Scan on s + Filter: (s.x = 'abc'::text COLLATE case_sensitive) + -> HashAggregate + Group Key: pushdown_ci.x + -> Append + -> Seq Scan on pushdown_ci + -> Seq Scan on pushdown_ci2 +(7 rows) + +SELECT * FROM (SELECT x FROM pushdown_ci UNION SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + x +--- +(0 rows) + +-- INTERSECT: same +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT x FROM pushdown_ci INTERSECT SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + QUERY PLAN +------------------------------------------------------ + Subquery Scan on s + Filter: (s.x = 'abc'::text COLLATE case_sensitive) + -> HashSetOp Intersect + -> Seq Scan on pushdown_ci + -> Seq Scan on pushdown_ci2 +(5 rows) + +SELECT * FROM (SELECT x FROM pushdown_ci INTERSECT SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + x +--- +(0 rows) + +-- INTERSECT ALL: still groups +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT x FROM pushdown_ci INTERSECT ALL SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + QUERY PLAN +------------------------------------------------------ + Subquery Scan on s + Filter: (s.x = 'abc'::text COLLATE case_sensitive) + -> HashSetOp Intersect All + -> Seq Scan on pushdown_ci + -> Seq Scan on pushdown_ci2 +(5 rows) + +SELECT * FROM (SELECT x FROM pushdown_ci INTERSECT ALL SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + x +--- +(0 rows) + +-- Negative: a function over a grouping column with a nondeterministic +-- collation, whose result is compared under no collation (an integer +-- comparison), can distinguish values the grouping considers equal. +-- PARTITION BY +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT id, x, count(*) OVER (PARTITION BY x) AS cnt FROM pushdown_ci +) s +WHERE ascii(x) = 97; + QUERY PLAN +---------------------------------------------------------------- + Subquery Scan on s + Filter: (ascii(s.x) = 97) + -> WindowAgg + Window: w1 AS (PARTITION BY pushdown_ci.x) + -> Sort + Sort Key: pushdown_ci.x COLLATE case_insensitive + -> Seq Scan on pushdown_ci +(7 rows) + +SELECT * FROM ( + SELECT id, x, count(*) OVER (PARTITION BY x) AS cnt FROM pushdown_ci +) s +WHERE ascii(x) = 97; + id | x | cnt +----+-----+----- + 2 | abc | 2 +(1 row) + +-- Same with DISTINCT +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT x FROM pushdown_ci) s WHERE ascii(x) = 97; + QUERY PLAN +------------------------------------- + Subquery Scan on s + Filter: (ascii(s.x) = 97) + -> HashAggregate + Group Key: pushdown_ci.x + -> Seq Scan on pushdown_ci +(5 rows) + +SELECT * FROM (SELECT DISTINCT x FROM pushdown_ci) s WHERE ascii(x) = 97; + x +--- +(0 rows) + +-- Same with Set operations +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT x FROM pushdown_ci UNION SELECT x FROM pushdown_ci2) s +WHERE ascii(x) = 97; + QUERY PLAN +-------------------------------------------- + Subquery Scan on s + Filter: (ascii(s.x) = 97) + -> HashAggregate + Group Key: pushdown_ci.x + -> Append + -> Seq Scan on pushdown_ci + -> Seq Scan on pushdown_ci2 +(7 rows) + +SELECT * FROM (SELECT x FROM pushdown_ci UNION SELECT x FROM pushdown_ci2) s +WHERE ascii(x) = 97; + x +--- +(0 rows) + +DROP TABLE pushdown_ci2; +DROP TABLE pushdown_ci; -- bpchar CREATE TABLE test1bpci (x char(3) COLLATE case_insensitive); CREATE TABLE test2bpci (x char(3) COLLATE case_insensitive); diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index 323429d896f..501ffadc105 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -1837,6 +1837,220 @@ NOTICE: x = 3, y = 0 drop function tattle(x int, y int); -- +-- check that an upper-level qual is not pushed down if its operator is from a +-- different btree opfamily than the subquery's grouping eqop +-- +BEGIN; +CREATE TYPE t_rec AS (x numeric); +CREATE TEMP TABLE pdt (id int, a t_rec); +INSERT INTO pdt VALUES + (1, ROW(1.00)::t_rec), + (2, ROW(1.0)::t_rec), + (3, ROW(2)::t_rec); +-- DISTINCT ON: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT ON (a) id, a FROM pdt ORDER BY a, id) s +WHERE a *= ROW(1.0)::t_rec; + QUERY PLAN +--------------------------------------- + Subquery Scan on s + Filter: (s.a *= '(1.0)'::t_rec) + -> Unique + -> Sort + Sort Key: pdt.a, pdt.id + -> Seq Scan on pdt +(6 rows) + +SELECT * FROM (SELECT DISTINCT ON (a) id, a FROM pdt ORDER BY a, id) s +WHERE a *= ROW(1.0)::t_rec; + id | a +----+--- +(0 rows) + +-- Window function PARTITION BY: conflict, qual stays outside the WindowAgg +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT id, a, count(*) OVER (PARTITION BY a) AS cnt FROM pdt +) s +WHERE a *= ROW(1.0)::t_rec; + QUERY PLAN +-------------------------------------------- + Subquery Scan on s + Filter: (s.a *= '(1.0)'::t_rec) + -> WindowAgg + Window: w1 AS (PARTITION BY pdt.a) + -> Sort + Sort Key: pdt.a + -> Seq Scan on pdt +(7 rows) + +SELECT * FROM ( + SELECT id, a, count(*) OVER (PARTITION BY a) AS cnt FROM pdt +) s +WHERE a *= ROW(1.0)::t_rec; + id | a | cnt +----+-------+----- + 2 | (1.0) | 2 +(1 row) + +-- Plain DISTINCT: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT a FROM pdt) s WHERE a *= ROW(1.0)::t_rec; + QUERY PLAN +----------------------------------- + Subquery Scan on s + Filter: (s.a *= '(1.0)'::t_rec) + -> HashAggregate + Group Key: pdt.a + -> Seq Scan on pdt +(5 rows) + +SELECT * FROM (SELECT DISTINCT a FROM pdt) s WHERE a *= ROW(1.0)::t_rec; + a +--- +(0 rows) + +-- Positive: compatible opfamily, safe to push past the grouping +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT ON (a) id, a FROM pdt ORDER BY a, id) s +WHERE a = ROW(1.0)::t_rec; + QUERY PLAN +-------------------------------------------- + Limit + -> Sort + Sort Key: pdt.id + -> Seq Scan on pdt + Filter: (a = '(1.0)'::t_rec) +(5 rows) + +SELECT * FROM (SELECT DISTINCT ON (a) id, a FROM pdt ORDER BY a, id) s +WHERE a = ROW(1.0)::t_rec; + id | a +----+-------- + 1 | (1.00) +(1 row) + +-- Set operations: any operation other than UNION ALL groups rows by equality, +-- so the same opfamily-mismatch rules apply. +CREATE TEMP TABLE u1 (a t_rec); +CREATE TEMP TABLE u2 (a t_rec); +INSERT INTO u1 VALUES (ROW(1.00)::t_rec), (ROW(1.0)::t_rec); +INSERT INTO u2 VALUES (ROW(1.0)::t_rec); +-- UNION: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT a FROM u1 UNION SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + QUERY PLAN +----------------------------------- + Subquery Scan on s + Filter: (s.a *= '(1.0)'::t_rec) + -> HashAggregate + Group Key: u1.a + -> Append + -> Seq Scan on u1 + -> Seq Scan on u2 +(7 rows) + +SELECT * FROM (SELECT a FROM u1 UNION SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + a +--- +(0 rows) + +-- INTERSECT: same +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT a FROM u1 INTERSECT SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + QUERY PLAN +----------------------------------- + Subquery Scan on s + Filter: (s.a *= '(1.0)'::t_rec) + -> HashSetOp Intersect + -> Seq Scan on u1 + -> Seq Scan on u2 +(5 rows) + +SELECT * FROM (SELECT a FROM u1 INTERSECT SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + a +--- +(0 rows) + +-- INTERSECT ALL: still groups +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT a FROM u1 INTERSECT ALL SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + QUERY PLAN +----------------------------------- + Subquery Scan on s + Filter: (s.a *= '(1.0)'::t_rec) + -> HashSetOp Intersect All + -> Seq Scan on u1 + -> Seq Scan on u2 +(5 rows) + +SELECT * FROM (SELECT a FROM u1 INTERSECT ALL SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + a +--- +(0 rows) + +-- UNION ALL of (UNION ...): an inner grouping node still exposes the +-- conflict to a qual pushed down through the outer UNION ALL. +EXPLAIN (COSTS OFF) +SELECT * FROM ( + (SELECT a FROM u1 UNION SELECT a FROM u2) + UNION ALL + SELECT a FROM u2 +) s +WHERE a *= ROW(1.0)::t_rec; + QUERY PLAN +---------------------------------------- + Subquery Scan on s + Filter: (s.a *= '(1.0)'::t_rec) + -> Append + -> HashAggregate + Group Key: u1.a + -> Append + -> Seq Scan on u1 + -> Seq Scan on u2 + -> Seq Scan on u2 u2_1 +(9 rows) + +SELECT * FROM ( + (SELECT a FROM u1 UNION SELECT a FROM u2) + UNION ALL + SELECT a FROM u2 +) s +WHERE a *= ROW(1.0)::t_rec; + a +------- + (1.0) +(1 row) + +-- UNION ALL only: no grouping anywhere, pushdown remains allowed. +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT a FROM u1 UNION ALL SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + QUERY PLAN +--------------------------------------- + Append + -> Seq Scan on u1 + Filter: (a *= '(1.0)'::t_rec) + -> Seq Scan on u2 + Filter: (a *= '(1.0)'::t_rec) +(5 rows) + +SELECT * FROM (SELECT a FROM u1 UNION ALL SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + a +------- + (1.0) + (1.0) +(2 rows) + +ROLLBACK; +-- -- Test that LIMIT can be pushed to SORT through a subquery that just projects -- columns. We check for that having happened by looking to see if EXPLAIN -- ANALYZE shows that a top-N sort was used. We must suppress or filter away diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index b751783e833..d9660cddbdf 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -539,27 +539,57 @@ alter table t2 alter column z drop not null; create unique index t2_z_uidx on t2(z) nulls not distinct; explain (costs off) select y,z from t2 group by y,z; +drop table t1 cascade; +drop table t2; +drop table t3; +drop table p_t1; + +-- A composite type used by the tests below to exercise the asymmetry +-- between record_ops (per-field equality, the default) and record_image_ops +-- (bytewise equality): values like row(1.0) and row(1.00) are field-equal +-- but byte-distinct. +create type avg_rec as (x numeric); + -- A unique index proves uniqueness only under its own opfamily. When the -- GROUP BY's eqop comes from a different opfamily with looser equality, -- rows the index regards as distinct can collapse into one GROUP BY group, -- so the index is not usable for removing redundant columns. -create type t_rec as (x numeric); -create temp table t_opf (a t_rec not null, b text); +create temp table t_opf (a avg_rec not null, b text); create unique index on t_opf (a record_image_ops); -- (1.0) and (1.00) are bytewise distinct but logically equal as records; -- the index admits both, but GROUP BY a (default record_ops) would merge -- them, so b must be retained as a grouping key. -insert into t_opf values (row(1.0)::t_rec, 'X'), (row(1.00)::t_rec, 'Y'); +insert into t_opf values (row(1.0)::avg_rec, 'X'), (row(1.00)::avg_rec, 'Y'); explain (costs off) select a, b from t_opf group by a, b order by b; select a, b from t_opf group by a, b order by b; drop table t_opf; -drop type t_rec; -drop table t1 cascade; -drop table t2; -drop table t3; -drop table p_t1; +-- A HAVING clause that uses an equality operator from a different opfamily +-- than the GROUP BY's eqop must NOT be pushed down to WHERE. +create temp table t_having (id int, a avg_rec); +insert into t_having values + (1, row(1.0)::avg_rec), + (2, row(1.00)::avg_rec), + (3, row(2)::avg_rec); + +-- the clause must stay in HAVING +explain (costs off) +select a, count(*) from t_having group by a having a *= row(1.0)::avg_rec; +select a, count(*) from t_having group by a having a *= row(1.0)::avg_rec; + +-- the clause must stay in HAVING +explain (costs off) +select a, count(*) from t_having group by a having a *= any (array[row(1.0)::avg_rec]); +select a, count(*) from t_having group by a having a *= any (array[row(1.0)::avg_rec]); + +-- the clause can be pushed down to WHERE +explain (costs off) +select a, count(*) from t_having group by a having a = row(1.0)::avg_rec; +select a, count(*) from t_having group by a having a = row(1.0)::avg_rec; + +drop table t_having; +drop type avg_rec; -- -- Test GROUP BY matching of join columns that are type-coerced due to USING diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index 90ea90d64e0..4e5ef47016c 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -757,31 +757,39 @@ EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive; SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive; --- Negative: function applied to grouped column with conflicting collation +-- Negative: function over the grouping column, conflicting collation EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x) = 'ABC' COLLATE case_sensitive; SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x) = 'ABC' COLLATE case_sensitive; --- Positive: function with same collation as GROUP BY +-- Negative: function over the grouping column whose result is compared as an +-- integer, under no collation +EXPLAIN (COSTS OFF) +SELECT x, count(*) FROM test3ci GROUP BY x HAVING ascii(x) = 97; +SELECT x, count(*) FROM test3ci GROUP BY x HAVING ascii(x) = 97; + +-- Negative: a function wrapping the grouping column is not provably safe even +-- when compared under the matching collation, since the function need not +-- preserve the collation's equality EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x) = 'ABC' COLLATE case_insensitive; SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x) = 'ABC' COLLATE case_insensitive; --- Negative: inner function has conflicting collation, even though outer --- operator's collation matches GROUP BY due to a COLLATE override +-- Negative: same, with the grouping column wrapped in a function whose input +-- collation is overridden; still not a direct operand, so it stays in HAVING EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x COLLATE case_sensitive) COLLATE case_insensitive = 'ABC'; SELECT x, count(*) FROM test3ci GROUP BY x HAVING upper(x COLLATE case_sensitive) COLLATE case_insensitive = 'ABC'; -- Mixed AND: conflicting clause stays in HAVING, safe clause pushed to WHERE EXPLAIN (COSTS OFF) -SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_sensitive AND length(x) > 1; -SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_sensitive AND length(x) > 1; +SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_sensitive AND x >= 'a' COLLATE case_insensitive; +SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_sensitive AND x >= 'a' COLLATE case_insensitive; -- Positive: AND of two safe clauses, both can be pushed EXPLAIN (COSTS OFF) -SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive AND length(x) > 1; -SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive AND length(x) > 1; +SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive AND x >= 'a' COLLATE case_insensitive; +SELECT x, count(*) FROM test3ci GROUP BY x HAVING x = 'abc' COLLATE case_insensitive AND x >= 'a' COLLATE case_insensitive; -- Negative: OR with a conflicting clause: must stay in HAVING EXPLAIN (COSTS OFF) @@ -822,6 +830,111 @@ EXPLAIN (COSTS OFF) SELECT x, count(*) FROM test3cs GROUP BY x HAVING x = 'abc' COLLATE case_insensitive ORDER BY 1; SELECT x, count(*) FROM test3cs GROUP BY x HAVING x = 'abc' COLLATE case_insensitive ORDER BY 1; +-- Test WHERE-pushdown past a grouping layer (DISTINCT, DISTINCT ON, window +-- PARTITION BY) when the qual applies a different collation than the +-- grouping column's nondeterministic collation. The qual would distinguish +-- rows the grouping considers equal, so it must NOT be pushed inside the +-- subquery. +CREATE TABLE pushdown_ci (id int, x text COLLATE case_insensitive); +INSERT INTO pushdown_ci VALUES (1, 'ABC'), (2, 'abc'), (3, 'def'); + +-- DISTINCT ON: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT ON (x) id, x FROM pushdown_ci ORDER BY x, id) s +WHERE x = 'abc' COLLATE case_sensitive; + +SELECT * FROM (SELECT DISTINCT ON (x) id, x FROM pushdown_ci ORDER BY x, id) s +WHERE x = 'abc' COLLATE case_sensitive; + +-- Window function PARTITION BY: conflict, qual stays outside the WindowAgg +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT id, x, count(*) OVER (PARTITION BY x) AS cnt FROM pushdown_ci +) s +WHERE x = 'abc' COLLATE case_sensitive; + +SELECT * FROM ( + SELECT id, x, count(*) OVER (PARTITION BY x) AS cnt FROM pushdown_ci +) s +WHERE x = 'abc' COLLATE case_sensitive; + +-- Plain DISTINCT: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT x FROM pushdown_ci) s +WHERE x = 'abc' COLLATE case_sensitive; + +SELECT * FROM (SELECT DISTINCT x FROM pushdown_ci) s +WHERE x = 'abc' COLLATE case_sensitive; + +-- Positive: matching collation, safe to push past the grouping +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT ON (x) id, x FROM pushdown_ci ORDER BY x, id) s +WHERE x = 'abc' COLLATE case_insensitive; + +SELECT * FROM (SELECT DISTINCT ON (x) id, x FROM pushdown_ci ORDER BY x, id) s +WHERE x = 'abc' COLLATE case_insensitive; + +-- Set operations: any operation other than UNION ALL groups rows by equality, +-- so the same collation-mismatch rules apply. +CREATE TABLE pushdown_ci2 (x text COLLATE case_insensitive); +INSERT INTO pushdown_ci2 VALUES ('abc'); + +-- UNION: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT x FROM pushdown_ci UNION SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + +SELECT * FROM (SELECT x FROM pushdown_ci UNION SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + +-- INTERSECT: same +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT x FROM pushdown_ci INTERSECT SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + +SELECT * FROM (SELECT x FROM pushdown_ci INTERSECT SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + +-- INTERSECT ALL: still groups +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT x FROM pushdown_ci INTERSECT ALL SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + +SELECT * FROM (SELECT x FROM pushdown_ci INTERSECT ALL SELECT x FROM pushdown_ci2) s +WHERE x = 'abc' COLLATE case_sensitive; + +-- Negative: a function over a grouping column with a nondeterministic +-- collation, whose result is compared under no collation (an integer +-- comparison), can distinguish values the grouping considers equal. +-- PARTITION BY +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT id, x, count(*) OVER (PARTITION BY x) AS cnt FROM pushdown_ci +) s +WHERE ascii(x) = 97; + +SELECT * FROM ( + SELECT id, x, count(*) OVER (PARTITION BY x) AS cnt FROM pushdown_ci +) s +WHERE ascii(x) = 97; + +-- Same with DISTINCT +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT x FROM pushdown_ci) s WHERE ascii(x) = 97; + +SELECT * FROM (SELECT DISTINCT x FROM pushdown_ci) s WHERE ascii(x) = 97; + +-- Same with Set operations +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT x FROM pushdown_ci UNION SELECT x FROM pushdown_ci2) s +WHERE ascii(x) = 97; + +SELECT * FROM (SELECT x FROM pushdown_ci UNION SELECT x FROM pushdown_ci2) s +WHERE ascii(x) = 97; + +DROP TABLE pushdown_ci2; +DROP TABLE pushdown_ci; + -- bpchar CREATE TABLE test1bpci (x char(3) COLLATE case_insensitive); CREATE TABLE test2bpci (x char(3) COLLATE case_insensitive); diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index c729d6d1970..10079f32ccd 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -925,6 +925,111 @@ select * from drop function tattle(x int, y int); +-- +-- check that an upper-level qual is not pushed down if its operator is from a +-- different btree opfamily than the subquery's grouping eqop +-- +BEGIN; + +CREATE TYPE t_rec AS (x numeric); +CREATE TEMP TABLE pdt (id int, a t_rec); +INSERT INTO pdt VALUES + (1, ROW(1.00)::t_rec), + (2, ROW(1.0)::t_rec), + (3, ROW(2)::t_rec); + +-- DISTINCT ON: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT ON (a) id, a FROM pdt ORDER BY a, id) s +WHERE a *= ROW(1.0)::t_rec; + +SELECT * FROM (SELECT DISTINCT ON (a) id, a FROM pdt ORDER BY a, id) s +WHERE a *= ROW(1.0)::t_rec; + +-- Window function PARTITION BY: conflict, qual stays outside the WindowAgg +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT id, a, count(*) OVER (PARTITION BY a) AS cnt FROM pdt +) s +WHERE a *= ROW(1.0)::t_rec; + +SELECT * FROM ( + SELECT id, a, count(*) OVER (PARTITION BY a) AS cnt FROM pdt +) s +WHERE a *= ROW(1.0)::t_rec; + +-- Plain DISTINCT: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT a FROM pdt) s WHERE a *= ROW(1.0)::t_rec; + +SELECT * FROM (SELECT DISTINCT a FROM pdt) s WHERE a *= ROW(1.0)::t_rec; + +-- Positive: compatible opfamily, safe to push past the grouping +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT DISTINCT ON (a) id, a FROM pdt ORDER BY a, id) s +WHERE a = ROW(1.0)::t_rec; + +SELECT * FROM (SELECT DISTINCT ON (a) id, a FROM pdt ORDER BY a, id) s +WHERE a = ROW(1.0)::t_rec; + +-- Set operations: any operation other than UNION ALL groups rows by equality, +-- so the same opfamily-mismatch rules apply. +CREATE TEMP TABLE u1 (a t_rec); +CREATE TEMP TABLE u2 (a t_rec); +INSERT INTO u1 VALUES (ROW(1.00)::t_rec), (ROW(1.0)::t_rec); +INSERT INTO u2 VALUES (ROW(1.0)::t_rec); + +-- UNION: conflict, qual stays in outer query +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT a FROM u1 UNION SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + +SELECT * FROM (SELECT a FROM u1 UNION SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + +-- INTERSECT: same +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT a FROM u1 INTERSECT SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + +SELECT * FROM (SELECT a FROM u1 INTERSECT SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + +-- INTERSECT ALL: still groups +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT a FROM u1 INTERSECT ALL SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + +SELECT * FROM (SELECT a FROM u1 INTERSECT ALL SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + +-- UNION ALL of (UNION ...): an inner grouping node still exposes the +-- conflict to a qual pushed down through the outer UNION ALL. +EXPLAIN (COSTS OFF) +SELECT * FROM ( + (SELECT a FROM u1 UNION SELECT a FROM u2) + UNION ALL + SELECT a FROM u2 +) s +WHERE a *= ROW(1.0)::t_rec; + +SELECT * FROM ( + (SELECT a FROM u1 UNION SELECT a FROM u2) + UNION ALL + SELECT a FROM u2 +) s +WHERE a *= ROW(1.0)::t_rec; + +-- UNION ALL only: no grouping anywhere, pushdown remains allowed. +EXPLAIN (COSTS OFF) +SELECT * FROM (SELECT a FROM u1 UNION ALL SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + +SELECT * FROM (SELECT a FROM u1 UNION ALL SELECT a FROM u2) s +WHERE a *= ROW(1.0)::t_rec; + +ROLLBACK; + -- -- Test that LIMIT can be pushed to SORT through a subquery that just projects -- columns. We check for that having happened by looking to see if EXPLAIN diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cd74c4e5b6..938befd5c80 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3667,7 +3667,9 @@ gistxlogPageDelete gistxlogPageReuse gistxlogPageSplit gistxlogPageUpdate +grouping_eqop_callback grouping_sets_data +grouping_walker_ctx growable_trgm_array gseg_picksplit_item gss_OID_set @@ -3680,7 +3682,7 @@ gss_key_value_set_desc gss_name_t gtrgm_consistent_cache gzFile -having_collation_ctx +having_grouping_ctx hbaPort heap_page_items_state help_handler From 0c06ebf126a0d5df39d9c4298193d9887443ce93 Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Mon, 6 Jul 2026 12:12:41 -0400 Subject: [PATCH 121/250] Prevent satisfies_hash_partition from crashing with VARIADIC NULL. Commit f3b0897a1213f46b4d3a99a7f8ef3a4b32e03572 fixed some related problems, but overlooked this one. That commit first appeared in PostgreSQL 11, so back-patch to all supported branches. Backpatch-through: 14 Discussion: http://postgr.es/m/CA+TgmobsvQw3F+KRYT83=N3teh8D2t-oPR=U06QDZJE3viCJRg@mail.gmail.com Reviewed-by: Tender Wang Reviewed-by: Ewan Young --- src/backend/partitioning/partbounds.c | 14 +++++++++++++- src/test/regress/expected/hash_part.out | 7 +++++++ src/test/regress/sql/hash_part.sql | 3 +++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c index 4bdc2941efb..a0a1363d5be 100644 --- a/src/backend/partitioning/partbounds.c +++ b/src/backend/partitioning/partbounds.c @@ -4868,6 +4868,12 @@ satisfies_hash_partition(PG_FUNCTION_ARGS) fcinfo->flinfo->fn_mcxt); } } + else if (PG_ARGISNULL(3)) + { + /* Special case for VARIADIC NULL::sometype[] */ + relation_close(parent, NoLock); + PG_RETURN_BOOL(false); + } else { ArrayType *variadic_array = PG_GETARG_ARRAYTYPE_P(3); @@ -4938,12 +4944,18 @@ satisfies_hash_partition(PG_FUNCTION_ARGS) } else { - ArrayType *variadic_array = PG_GETARG_ARRAYTYPE_P(3); + ArrayType *variadic_array; int i; int nelems; Datum *datum; bool *isnull; + /* Special case for VARIADIC NULL::sometype[] */ + if (PG_ARGISNULL(3)) + PG_RETURN_BOOL(false); + + variadic_array = PG_GETARG_ARRAYTYPE_P(3); + deconstruct_array(variadic_array, my_extra->variadic_type, my_extra->variadic_typlen, diff --git a/src/test/regress/expected/hash_part.out b/src/test/regress/expected/hash_part.out index cb39161f867..44b6e461ffe 100644 --- a/src/test/regress/expected/hash_part.out +++ b/src/test/regress/expected/hash_part.out @@ -40,6 +40,13 @@ SELECT satisfies_hash_partition('mchash'::regclass, 4, NULL, NULL); f (1 row) +-- variadic null +SELECT satisfies_hash_partition('mchash'::regclass, 4, 0, VARIADIC NULL::int[]); + satisfies_hash_partition +-------------------------- + f +(1 row) + -- too many arguments SELECT satisfies_hash_partition('mchash'::regclass, 4, 0, NULL::int, NULL::text, NULL::json); ERROR: number of partitioning columns (2) does not match number of partition keys provided (3) diff --git a/src/test/regress/sql/hash_part.sql b/src/test/regress/sql/hash_part.sql index 6e2c1f21bfc..7243299d962 100644 --- a/src/test/regress/sql/hash_part.sql +++ b/src/test/regress/sql/hash_part.sql @@ -35,6 +35,9 @@ SELECT satisfies_hash_partition('mchash'::regclass, NULL, 0, NULL); -- remainder is null SELECT satisfies_hash_partition('mchash'::regclass, 4, NULL, NULL); +-- variadic null +SELECT satisfies_hash_partition('mchash'::regclass, 4, 0, VARIADIC NULL::int[]); + -- too many arguments SELECT satisfies_hash_partition('mchash'::regclass, 4, 0, NULL::int, NULL::text, NULL::json); From d0bb49e61168953f0f07c3beeb2b8fb63a3521a2 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 6 Jul 2026 13:06:21 -0400 Subject: [PATCH 122/250] Fix LIKE/regex optimization for indexscan with exact-match pattern. Commit 85b7efa1c introduced support for LIKE with non-deterministic collations. By moving some conditionals around, it accidentally broke the optimization for converting a LIKE or regex exact-match pattern to an equality indexqual when the index collation doesn't match the expression collation. That should be allowed if the expression collation is deterministic. This patch re-introduces the optimization for that common case. One important beneficiary of this optimization is the "\d tablename" command in psql. Without this fix that will do a seqscan on pg_class instead of an index point lookup. Reported-by: Andres Freund Author: Jelte Fennema-Nio Reviewed-by: Tom Lane Discussion: https://postgr.es/m/DHBQIZX8SZVI.ZX614ZMFL645@jeltef.nl Backpatch-through: 18 --- src/backend/utils/adt/like_support.c | 20 ++++++++++++---- .../regress/expected/collate.icu.utf8.out | 23 +++++++++++++++++++ src/test/regress/expected/collate.out | 23 +++++++++++++++++++ src/test/regress/sql/collate.icu.utf8.sql | 10 ++++++++ src/test/regress/sql/collate.sql | 10 ++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/backend/utils/adt/like_support.c b/src/backend/utils/adt/like_support.c index 8fdc677371f..eb1dcc12dfe 100644 --- a/src/backend/utils/adt/like_support.c +++ b/src/backend/utils/adt/like_support.c @@ -69,6 +69,10 @@ typedef enum Pattern_Prefix_None, Pattern_Prefix_Partial, Pattern_Prefix_Exact, } Pattern_Prefix_Status; +/* non-collatable comparisons, eg for bytea, are always deterministic */ +#define NONDETERMINISTIC(coll) \ + (OidIsValid(coll) && !get_collation_isdeterministic(coll)) + static Node *like_regex_support(Node *rawreq, Pattern_Type ptype); static List *match_pattern_prefix(Node *leftop, Node *rightop, @@ -383,12 +387,22 @@ match_pattern_prefix(Node *leftop, * us to not be concerned with specific opclasses (except for the legacy * "pattern" cases); any index that correctly implements the operators * will work. + * + * This case will work for LIKE/regex expressions with nondeterministic + * collation, so long as the index's collation is the same. If the + * expression's collation is deterministic, we can even use an index whose + * collation differs from the expression's. All deterministic collations + * agree on equality (it's bitwise), while we assume that an index with + * nondeterministic collation will return a superset of the bitwise-equal + * entries. Since the "=" indexqual is marked as lossy by default, we'll + * apply the LIKE/regex operator as a recheck, and that will filter out + * any non-matching entries. */ if (pstatus == Pattern_Prefix_Exact) { if (!op_in_opfamily(eqopr, opfamily)) return NIL; - if (indexcollation != expr_coll) + if (indexcollation != expr_coll && NONDETERMINISTIC(expr_coll)) return NIL; expr = make_opclause(eqopr, BOOLOID, false, (Expr *) leftop, (Expr *) prefix, @@ -402,10 +416,8 @@ match_pattern_prefix(Node *leftop, * expression collation is nondeterministic. The optimized equality or * prefix tests use bytewise comparisons, which is not consistent with * nondeterministic collations. - * - * expr_coll is not set for a non-collation-aware data type such as bytea. */ - if (expr_coll && !get_collation_isdeterministic(expr_coll)) + if (NONDETERMINISTIC(expr_coll)) return NIL; /* diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index ba247282476..f8e65313752 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -2074,6 +2074,29 @@ SELECT string_to_array('ABCDEFGHI' COLLATE case_insensitive, NULL, 'b'); {A,NULL,C,D,E,F,G,H,I} (1 row) +-- These queries should be able to use the index on test1ci.x: +SET enable_seqscan = off; +SET enable_indexonlyscan = off; +EXPLAIN (COSTS OFF) +SELECT * FROM test1ci WHERE x ~ '^abc$' COLLATE "C"; + QUERY PLAN +------------------------------------------- + Index Scan using test1ci_x_idx on test1ci + Index Cond: (x = 'abc'::text) + Filter: (x ~ '^abc$'::text COLLATE "C") +(3 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM test1ci WHERE x LIKE 'abc' COLLATE case_insensitive; + QUERY PLAN +------------------------------------------------------- + Index Scan using test1ci_x_idx on test1ci + Index Cond: (x = 'abc'::text) + Filter: (x ~~ 'abc'::text COLLATE case_insensitive) +(3 rows) + +RESET enable_seqscan; +RESET enable_indexonlyscan; -- Test HAVING-to-WHERE pushdown with nondeterministic collations. -- When a HAVING clause uses a different collation than the GROUP BY's -- nondeterministic collation, it must not be pushed to WHERE, otherwise diff --git a/src/test/regress/expected/collate.out b/src/test/regress/expected/collate.out index bf72908fbd3..a57c865bb0f 100644 --- a/src/test/regress/expected/collate.out +++ b/src/test/regress/expected/collate.out @@ -766,6 +766,29 @@ DETAIL: LOCALE cannot be specified together with LC_COLLATE or LC_CTYPE. CREATE COLLATION coll_dup_chk (FROM = "C", VERSION = "1"); ERROR: conflicting or redundant options DETAIL: FROM cannot be specified together with any other options. +-- Regex exact-match optimization should use an index even when the expression +-- and index have different collations, so long as the expression's collation +-- is deterministic. This example tests what we want because the optimizer +-- does not perceive "C" collation (used by the system catalogs) as identical +-- to "POSIX" collation. +EXPLAIN (COSTS OFF) +SELECT * FROM pg_class WHERE relname ~ '^pg_class$' COLLATE "POSIX"; + QUERY PLAN +---------------------------------------------------------- + Index Scan using pg_class_relname_nsp_index on pg_class + Index Cond: (relname = 'pg_class'::text) + Filter: (relname ~ '^pg_class$'::text COLLATE "POSIX") +(3 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM pg_class WHERE relname LIKE 'pg\_class' COLLATE "POSIX"; + QUERY PLAN +---------------------------------------------------------- + Index Scan using pg_class_relname_nsp_index on pg_class + Index Cond: (relname = 'pg_class'::text) + Filter: (relname ~~ 'pg\_class'::text COLLATE "POSIX") +(3 rows) + -- -- Clean up. Many of these table names will be re-used if the user is -- trying to run any platform-specific collation tests later, so we diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index 4e5ef47016c..f4c4271fb34 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -741,6 +741,16 @@ CREATE UNIQUE INDEX ON test3ci (x); -- error SELECT string_to_array('ABC,DEF,GHI' COLLATE case_insensitive, ',', 'abc'); SELECT string_to_array('ABCDEFGHI' COLLATE case_insensitive, NULL, 'b'); +-- These queries should be able to use the index on test1ci.x: +SET enable_seqscan = off; +SET enable_indexonlyscan = off; +EXPLAIN (COSTS OFF) +SELECT * FROM test1ci WHERE x ~ '^abc$' COLLATE "C"; +EXPLAIN (COSTS OFF) +SELECT * FROM test1ci WHERE x LIKE 'abc' COLLATE case_insensitive; +RESET enable_seqscan; +RESET enable_indexonlyscan; + -- Test HAVING-to-WHERE pushdown with nondeterministic collations. -- When a HAVING clause uses a different collation than the GROUP BY's -- nondeterministic collation, it must not be pushed to WHERE, otherwise diff --git a/src/test/regress/sql/collate.sql b/src/test/regress/sql/collate.sql index 4b0e4472c3f..b018da13f24 100644 --- a/src/test/regress/sql/collate.sql +++ b/src/test/regress/sql/collate.sql @@ -302,6 +302,16 @@ CREATE COLLATION coll_dup_chk (LC_CTYPE = "POSIX", LOCALE = ''); -- FROM conflicts with any other option CREATE COLLATION coll_dup_chk (FROM = "C", VERSION = "1"); +-- Regex exact-match optimization should use an index even when the expression +-- and index have different collations, so long as the expression's collation +-- is deterministic. This example tests what we want because the optimizer +-- does not perceive "C" collation (used by the system catalogs) as identical +-- to "POSIX" collation. +EXPLAIN (COSTS OFF) +SELECT * FROM pg_class WHERE relname ~ '^pg_class$' COLLATE "POSIX"; +EXPLAIN (COSTS OFF) +SELECT * FROM pg_class WHERE relname LIKE 'pg\_class' COLLATE "POSIX"; + -- -- Clean up. Many of these table names will be re-used if the user is -- trying to run any platform-specific collation tests later, so we From a99bd8d584ea268d8a3d02d0f0facb74429ffcf9 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 6 Jul 2026 14:35:21 -0400 Subject: [PATCH 123/250] Fix LIKE matching with nondeterministic collations and backslashes. Commit 85b7efa1c added support for LIKE with nondeterministic collations, but it included a bug in the de-escaping logic for literal pattern substrings. That unconditionally skipped all backslashes, but when it encounters '\\' it should emit the second backslash as a de-escaped character. That led to acting as though the escaped backslash was not there. Bug: #19474 Reported-by: Bowen Shi Author: Nitin Motiani Reviewed-by: Zsolt Parragi Reviewed-by: Ewan Young Reviewed-by: Tom Lane Discussion: https://postgr.es/m/19474-5b86a95f3d9a7ecb@postgresql.org Discussion: https://postgr.es/m/CAH5HC94yU+K8Gcdy12M5BS8gwD_SXLSHzc9k5tNk7JDnpBiFMA@mail.gmail.com Backpatch-through: 18 --- src/backend/utils/adt/like_match.c | 5 ++- .../regress/expected/collate.icu.utf8.out | 31 +++++++++++++++++++ src/test/regress/sql/collate.icu.utf8.sql | 7 +++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/adt/like_match.c b/src/backend/utils/adt/like_match.c index bac6c182467..4f74bf3cbcb 100644 --- a/src/backend/utils/adt/like_match.c +++ b/src/backend/utils/adt/like_match.c @@ -253,9 +253,8 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) for (const char *c = p; c < p1; c++) { if (*c == '\\') - ; - else - *(b++) = *c; + c++; /* we already checked this isn't the end */ + *(b++) = *c; } subpat = buf; diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index f8e65313752..2d17d56f6d0 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -2996,6 +2996,37 @@ SELECT U&'\0061\0308bc' LIKE U&'_\00e4bc' COLLATE ignore_accents; -- escape character at end of pattern SELECT 'foox' LIKE 'foo\' COLLATE ignore_accents; ERROR: LIKE pattern must not end with escape character +-- literal backslash with nondeterministic collation (bug #19474) +SELECT 'back\slash' COLLATE ignore_accents LIKE 'back\slash%' ESCAPE '#'; + ?column? +---------- + t +(1 row) + +SELECT 'aäb' COLLATE ignore_accents LIKE 'a#äb' ESCAPE '#' AS multibyte_escape; + multibyte_escape +------------------ + t +(1 row) + +SELECT 'a\äb' COLLATE ignore_accents LIKE 'a\äb%' ESCAPE '#' AS backslash_multibyte; + backslash_multibyte +--------------------- + t +(1 row) + +SELECT 'a\b%c' COLLATE ignore_accents LIKE 'a#\b#%%c' ESCAPE '#' AS mixed_escapes; + mixed_escapes +--------------- + t +(1 row) + +SELECT 'backslash' COLLATE ignore_accents LIKE 'back\\slash%'; + ?column? +---------- + f +(1 row) + -- foreign keys (mixing different nondeterministic collations not allowed) CREATE TABLE test10pk (x text COLLATE case_sensitive PRIMARY KEY); CREATE TABLE test10fk (x text COLLATE case_insensitive REFERENCES test10pk (x) ON UPDATE CASCADE ON DELETE CASCADE); -- error diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index f4c4271fb34..4fb01a17298 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -1079,6 +1079,13 @@ SELECT U&'\0061\0308bc' LIKE U&'_\00e4bc' COLLATE ignore_accents; -- escape character at end of pattern SELECT 'foox' LIKE 'foo\' COLLATE ignore_accents; +-- literal backslash with nondeterministic collation (bug #19474) +SELECT 'back\slash' COLLATE ignore_accents LIKE 'back\slash%' ESCAPE '#'; +SELECT 'aäb' COLLATE ignore_accents LIKE 'a#äb' ESCAPE '#' AS multibyte_escape; +SELECT 'a\äb' COLLATE ignore_accents LIKE 'a\äb%' ESCAPE '#' AS backslash_multibyte; +SELECT 'a\b%c' COLLATE ignore_accents LIKE 'a#\b#%%c' ESCAPE '#' AS mixed_escapes; +SELECT 'backslash' COLLATE ignore_accents LIKE 'back\\slash%'; + -- foreign keys (mixing different nondeterministic collations not allowed) CREATE TABLE test10pk (x text COLLATE case_sensitive PRIMARY KEY); CREATE TABLE test10fk (x text COLLATE case_insensitive REFERENCES test10pk (x) ON UPDATE CASCADE ON DELETE CASCADE); -- error From 51652c42da2e982fb7028970eea5eed4c670a555 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 6 Jul 2026 14:47:58 -0400 Subject: [PATCH 124/250] Fix mishandling of leading '\' in nondeterministic LIKE. The loop in MatchText() processed a leading '\' without regard to nondeterministic locales, which is problematic if what the '\' precedes is an ordinary character that should be subject to nondeterministic matching. We'd insist on a literal match for it, which is not right and is not like what happens with a '\' that follows some ordinary characters. Worse, we'd then advance the text and pattern pointers by one byte, so that if the escaped character is multibyte the next loop iteration would take the nondeterministic code path starting at a point within the character. That could very possibly cause pg_strncoll() to misbehave. The fix is quite simple: move the stanza that handles '\' down past the one that handles nondeterminism. The stanzas for '%' and '_' are fine where they are, but the '\' stanza is only correct for deterministic matching. The logic for nondeterministic cases is already prepared to do the right things with a '\'. While here, I replaced tests of "locale && !locale->deterministic" with a boolean local variable, reasoning that those are in the hot loop paths so saving a branch and indirect fetch is worth the trouble. I also improved a number of related comments. Author: Tom Lane Discussion: https://postgr.es/m/391592.1783187986@sss.pgh.pa.us Backpatch-through: 18 --- src/backend/utils/adt/like_match.c | 76 +++++++++++-------- .../regress/expected/collate.icu.utf8.out | 30 ++++++++ src/test/regress/sql/collate.icu.utf8.sql | 6 ++ 3 files changed, 80 insertions(+), 32 deletions(-) diff --git a/src/backend/utils/adt/like_match.c b/src/backend/utils/adt/like_match.c index 4f74bf3cbcb..6735d7fa775 100644 --- a/src/backend/utils/adt/like_match.c +++ b/src/backend/utils/adt/like_match.c @@ -79,6 +79,8 @@ static int MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) { + bool nondeterministic = (locale && !locale->deterministic); + /* Fast path for match-everything pattern */ if (plen == 1 && *p == '%') return LIKE_TRUE; @@ -92,23 +94,16 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) * occasions it is safe to advance by byte, as the text and pattern will * be in lockstep. This allows us to perform all comparisons between the * text and pattern on a byte by byte basis, even for multi-byte - * encodings. + * encodings. (But that doesn't work in a nondeterministic locale, so the + * nondeterministic case below has to advance the text by chars.) */ while (tlen > 0 && plen > 0) { - if (*p == '\\') - { - /* Next pattern byte must match literally, whatever it is */ - NextByte(p, plen); - /* ... and there had better be one, per SQL standard */ - if (plen <= 0) - ereport(ERROR, - (errcode(ERRCODE_INVALID_ESCAPE_SEQUENCE), - errmsg("LIKE pattern must not end with escape character"))); - if (GETCHAR(*p, locale) != GETCHAR(*t, locale)) - return LIKE_FALSE; - } - else if (*p == '%') + /* + * At the top of this loop, we are not positioned immediately after an + * escape, so we may take wildcards at face value. + */ + if (*p == '%') { char firstpat; @@ -157,9 +152,9 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) * the first pattern byte to each text byte to avoid recursing * more than we have to. This fact also guarantees that we don't * have to consider a match to the zero-length substring at the - * end of the text. With a nondeterministic collation, we can't - * rely on the first bytes being equal, so we have to recurse in - * any case. + * end of the text. But with a nondeterministic locale, we can't + * rely on the first byte of a match being equal, so we have to + * recurse in any case. */ if (*p == '\\') { @@ -174,7 +169,7 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) while (tlen > 0) { - if (GETCHAR(*t, locale) == firstpat || (locale && !locale->deterministic)) + if (GETCHAR(*t, locale) == firstpat || nondeterministic) { int matched = MatchText(t, tlen, p, plen, locale); @@ -198,7 +193,7 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) NextByte(p, plen); continue; } - else if (locale && !locale->deterministic) + else if (nondeterministic) { /* * For nondeterministic locales, we find the next substring of the @@ -218,9 +213,9 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) char *buf = NULL; /* - * Determine next substring of pattern without wildcards. p is - * the start of the subpattern, p1 is one past the last byte. Also - * track if we found an escape character. + * Determine length of substring of pattern without wildcards. p + * is the start of the subpattern, p1 will advance to one past its + * last byte. Also track if we found an escape character. */ p1 = p; p1len = plen; @@ -238,12 +233,15 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) } else if (*p1 == '_' || *p1 == '%') break; + /* Advance over regular or escaped character */ NextByte(p1, p1len); } /* - * If we found an escape character, then make an unescaped copy of - * the subpattern. + * If we found an escape character, then make a de-escaped copy of + * the subpattern that we can use to match literally. Otherwise + * we can use the subpattern in-place. (buf holds the de-escaped + * copy; be sure to pfree it before returning.) */ if (found_escape) { @@ -285,9 +283,10 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) } /* - * Now build a substring of the text and try to match it against - * the subpattern. t is the start of the text, t1 is one past the - * last byte. We start with a zero-length string. + * Consider each successively-longer substring of the remaining + * text and try to match it against the subpattern. t is the + * start of the substring, t1 is one past its last byte. We start + * with a zero-length substring. */ t1 = t; t1len = tlen; @@ -295,16 +294,16 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) { int cmp; + /* This could be slow, so allow interrupts */ CHECK_FOR_INTERRUPTS(); cmp = pg_strncoll(subpat, subpatlen, t, (t1 - t), locale); /* * If we found a match, we have to test if the rest of pattern - * can match against the rest of the string. Otherwise we - * have to continue here try matching with a longer substring. - * (This is similar to the recursion for the '%' wildcard - * above.) + * can match against the rest of the text. If not, we have to + * continue and try the next longer substring. (This is + * similar to the recursion for the '%' wildcard above.) * * Note that we can't just wind forward p and t and continue * with the main loop. This would fail for example with @@ -339,7 +338,20 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) } else NextChar(t1, t1len); - } + } /* end loop over substrings starting at t */ + } + /* the rest of this loop considers only deterministic cases */ + else if (*p == '\\') + { + /* Next pattern byte must match literally, whatever it is */ + NextByte(p, plen); + /* ... and there had better be one, per SQL standard */ + if (plen <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_ESCAPE_SEQUENCE), + errmsg("LIKE pattern must not end with escape character"))); + if (GETCHAR(*p, locale) != GETCHAR(*t, locale)) + return LIKE_FALSE; } else if (GETCHAR(*p, locale) != GETCHAR(*t, locale)) { diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index 2d17d56f6d0..e700a5e1122 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -1471,6 +1471,36 @@ SELECT 'abc' <= 'ABC' COLLATE case_insensitive, 'abc' >= 'ABC' COLLATE case_inse t | t (1 row) +SELECT 'AB' LIKE 'ab' COLLATE case_insensitive AS t; + t +--- + t +(1 row) + +SELECT 'AB' LIKE 'a\b' COLLATE case_insensitive AS t; + t +--- + t +(1 row) + +SELECT 'AB' LIKE '\ab' COLLATE case_insensitive AS t; + t +--- + t +(1 row) + +SELECT 'AB' LIKE '\a%' COLLATE case_insensitive AS t; + t +--- + t +(1 row) + +SELECT 'AB' LIKE '\a\%' COLLATE case_insensitive AS f; + f +--- + f +(1 row) + -- tests with array_sort SELECT array_sort('{a,B}'::text[] COLLATE case_insensitive); array_sort diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index 4fb01a17298..a17e8ae27f9 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -564,6 +564,12 @@ CREATE COLLATION case_insensitive (provider = icu, locale = '@colStrength=second SELECT 'abc' <= 'ABC' COLLATE case_sensitive, 'abc' >= 'ABC' COLLATE case_sensitive; SELECT 'abc' <= 'ABC' COLLATE case_insensitive, 'abc' >= 'ABC' COLLATE case_insensitive; +SELECT 'AB' LIKE 'ab' COLLATE case_insensitive AS t; +SELECT 'AB' LIKE 'a\b' COLLATE case_insensitive AS t; +SELECT 'AB' LIKE '\ab' COLLATE case_insensitive AS t; +SELECT 'AB' LIKE '\a%' COLLATE case_insensitive AS t; +SELECT 'AB' LIKE '\a\%' COLLATE case_insensitive AS f; + -- tests with array_sort SELECT array_sort('{a,B}'::text[] COLLATE case_insensitive); SELECT array_sort('{a,B}'::text[] COLLATE "C"); From 441e4c8d699910ea21f7f31ae7e76dbc1e572e05 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Tue, 7 Jul 2026 08:13:42 +0900 Subject: [PATCH 125/250] Enforce RETURNING typmod on SQL/JSON DEFAULT behavior expressions transformJsonBehavior() coerced an ON EMPTY / ON ERROR DEFAULT expression only when its type differed from the RETURNING type's OID. When the base type matched but the RETURNING type carried a type modifier (e.g. numeric(4,1) or varchar(3)), the coercion that enforces the typmod was skipped, so the DEFAULT value could violate the declared type: SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT 99999.999 ON EMPTY); returned 99999.999, which 99999.999::numeric(4,1) would reject; the value could even be stored into a numeric(4,1) column, as later coercions trust its already-correct type label. Fix by also coercing when the RETURNING type has a typmod, except for a NULL constant. coerce_to_target_type() is a no-op when the typmod already matches. The matching-OID short-circuit dates to 74c96699be3. Reported-by: Ewan Young Author: Ewan Young Discussion: https://postgr.es/m/CAON2xHPO9f4cAmyGn1mQ=VqoS7wN5rz4yOiqudxX78zninZpCw@mail.gmail.com Backpatch-through: 17 --- src/backend/parser/parse_expr.c | 11 +++++++++- .../regress/expected/sqljson_jsontable.out | 16 ++++++++++++++ .../regress/expected/sqljson_queryfuncs.out | 21 +++++++++++++++++++ src/test/regress/sql/sqljson_jsontable.sql | 9 ++++++++ src/test/regress/sql/sqljson_queryfuncs.sql | 8 +++++++ 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index a00b46a25ca..cf872ce79b5 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4819,8 +4819,17 @@ transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr, * * For other non-NULL expressions, try to find a cast and error out if one * is not found. + * + * The DEFAULT expression's base type may already match the RETURNING type + * yet still need coercion: when the RETURNING type carries a type + * modifier (e.g. numeric(4,1)), the cast below is what enforces it, so + * skipping it here would let the DEFAULT yield a value that violates its + * declared RETURNING type. A NULL constant needs no such enforcement. */ - if (expr && exprType(expr) != returning->typid) + if (expr && + (exprType(expr) != returning->typid || + (returning->typmod >= 0 && + !(IsA(expr, Const) && ((Const *) expr)->constisnull)))) { bool isnull = (IsA(expr, Const) && ((Const *) expr)->constisnull); diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out index 458c5aaa5b0..4d500e7de2d 100644 --- a/src/test/regress/expected/sqljson_jsontable.out +++ b/src/test/regress/expected/sqljson_jsontable.out @@ -250,6 +250,22 @@ SELECT * FROM JSON_TABLE(jsonb '{"d1": "foo"}', '$' {1} (1 row) +-- A DEFAULT expression whose base type matches the column type must still be +-- coerced to the column's typmod. +SELECT * FROM JSON_TABLE(jsonb '{}', '$' + COLUMNS (c numeric(4,1) PATH '$.x' DEFAULT 99999.999 ON EMPTY)); +ERROR: numeric field overflow +DETAIL: A field with precision 4, scale 1 must round to an absolute value less than 10^3. +SELECT * FROM JSON_TABLE(jsonb '{}', '$' + COLUMNS (c bit(3) PATH '$.x' DEFAULT b'10101' ON EMPTY)); +ERROR: bit string length 5 does not match type bit(3) +SELECT * FROM JSON_TABLE(jsonb '{}', '$' + COLUMNS (c numeric(4,1) PATH '$.x' DEFAULT abs(NULL::numeric) ON EMPTY)); + c +--- + +(1 row) + -- JSON_TABLE: Test backward parsing CREATE VIEW jsonb_table_view2 AS SELECT * FROM diff --git a/src/test/regress/expected/sqljson_queryfuncs.out b/src/test/regress/expected/sqljson_queryfuncs.out index 53145f50f18..822855c4a3c 100644 --- a/src/test/regress/expected/sqljson_queryfuncs.out +++ b/src/test/regress/expected/sqljson_queryfuncs.out @@ -433,6 +433,27 @@ SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING ERROR: cannot specify FORMAT JSON in RETURNING clause of JSON_VALUE() LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... ^ +-- A DEFAULT expression must be coerced to the RETURNING type's typmod even +-- when its base type already matches, but a matching NULL needs no coercion. +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT 99999.999 ON EMPTY); +ERROR: numeric field overflow +DETAIL: A field with precision 4, scale 1 must round to an absolute value less than 10^3. +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING varchar(3) DEFAULT 'toolong'::varchar(10) ON EMPTY); +ERROR: value too long for type character varying(3) +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT NULL::numeric ON EMPTY); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING bit(3) DEFAULT b'10101' ON EMPTY); +ERROR: bit string length 5 does not match type bit(3) +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT abs(NULL::numeric) ON EMPTY); + json_value +------------ + +(1 row) + -- RETUGNING pseudo-types not allowed SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING record); ERROR: returning pseudo-types is not supported in SQL/JSON functions diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql index 154eea79c76..41824094b96 100644 --- a/src/test/regress/sql/sqljson_jsontable.sql +++ b/src/test/regress/sql/sqljson_jsontable.sql @@ -132,6 +132,15 @@ SELECT * FROM JSON_TABLE(jsonb '{"d1": "foo"}', '$' SELECT * FROM JSON_TABLE(jsonb '{"d1": "foo"}', '$' COLUMNS (js1 oid[] PATH '$.d2' DEFAULT '{1}'::int[]::oid[] ON EMPTY)); +-- A DEFAULT expression whose base type matches the column type must still be +-- coerced to the column's typmod. +SELECT * FROM JSON_TABLE(jsonb '{}', '$' + COLUMNS (c numeric(4,1) PATH '$.x' DEFAULT 99999.999 ON EMPTY)); +SELECT * FROM JSON_TABLE(jsonb '{}', '$' + COLUMNS (c bit(3) PATH '$.x' DEFAULT b'10101' ON EMPTY)); +SELECT * FROM JSON_TABLE(jsonb '{}', '$' + COLUMNS (c numeric(4,1) PATH '$.x' DEFAULT abs(NULL::numeric) ON EMPTY)); + -- JSON_TABLE: Test backward parsing CREATE VIEW jsonb_table_view2 AS diff --git a/src/test/regress/sql/sqljson_queryfuncs.sql b/src/test/regress/sql/sqljson_queryfuncs.sql index a5d5e256d7f..879cc7db002 100644 --- a/src/test/regress/sql/sqljson_queryfuncs.sql +++ b/src/test/regress/sql/sqljson_queryfuncs.sql @@ -105,6 +105,14 @@ SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +-- A DEFAULT expression must be coerced to the RETURNING type's typmod even +-- when its base type already matches, but a matching NULL needs no coercion. +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT 99999.999 ON EMPTY); +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING varchar(3) DEFAULT 'toolong'::varchar(10) ON EMPTY); +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT NULL::numeric ON EMPTY); +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING bit(3) DEFAULT b'10101' ON EMPTY); +SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT abs(NULL::numeric) ON EMPTY); + -- RETUGNING pseudo-types not allowed SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING record); From cf184ec77a04bf164692a0c7bcd148f1e96b71f2 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Tue, 7 Jul 2026 23:59:08 +1200 Subject: [PATCH 126/250] Fix COUNT's logic for window run condition support 9d9c02ccd added code to allow the executor to stop early when processing WindowAgg nodes where a monotonic window function starts producing values that result in a pushed-down qual no longer matching, and will never match again due to the window function's monotonic properties. That commit requires a SupportRequestWFuncMonotonic to exist on the window function and for it to detect when the function is monotonic. For COUNT(ANY) and COUNT(*), the support function failed to consider some cases where the WindowClause used EXCLUDE to exclude certain rows from being aggregated. Some WindowClause definitions mean we aggregate rows that come after the current row, and when processing those rows later, if we EXCLUDE certain rows, the monotonic property can be broken. Wrongly treating the COUNT(*) or COUNT(ANY) aggregate as monotonic could lead to rows being filtered that should not be filtered from the result set. Another issue was that the support function for the COUNT aggregate mistakenly thought that a WindowClause without an ORDER BY meant that the results would be both monotonically increasing and decreasing, but that's only true when in RANGE mode, where all rows are peers. It is possible to support various cases that do have an EXCLUDE clause, but getting the logic correct for the exact set of cases that are valid is quite complex and would likely better be left for a future project. Here, we mostly disable run condition pushdown when there is an EXCLUDE clause unless the clause is for EXCLUDE CURRENT ROW, uses COUNT(*) (rather than COUNT(ANY)), and the window aggregate has no FILTER clause. Bug: #19533 Reported-by: Qifan Liu Author: Chengpeng Yan Author: David Rowley Reviewed-by: Richard Guo Reviewed-by: John Naylor Discussion: https://postgr.es/m/19533-413a1014e5d0e766@postgresql.org Backpatch-through: 15 --- src/backend/utils/adt/int8.c | 36 ++++- src/test/regress/expected/window.out | 221 ++++++++++++++++++++++++++- src/test/regress/sql/window.sql | 115 +++++++++++++- 3 files changed, 362 insertions(+), 10 deletions(-) diff --git a/src/backend/utils/adt/int8.c b/src/backend/utils/adt/int8.c index 9dd5889f34c..ad6a56b4688 100644 --- a/src/backend/utils/adt/int8.c +++ b/src/backend/utils/adt/int8.c @@ -24,7 +24,7 @@ #include "nodes/supportnodes.h" #include "optimizer/optimizer.h" #include "utils/builtins.h" - +#include "utils/fmgroids.h" typedef struct { @@ -833,8 +833,38 @@ int8inc_support(PG_FUNCTION_ARGS) MonotonicFunction monotonic = MONOTONICFUNC_NONE; int frameOptions = req->window_clause->frameOptions; - /* No ORDER BY clause then all rows are peers */ - if (req->window_clause->orderClause == NIL) + /* + * Because an EXCLUDE clauses in the window definition can exclude + * rows that have previously been included in the aggregate result for + * prior rows, this can break the monotonic properties that might + * otherwise be guaranteed. There's a narrow set of circumstances + * that can be guaranteed, which we check for below. + */ + if (frameOptions & FRAMEOPTION_EXCLUSION) + { + WindowFunc *wfunc = req->window_func; + + /* + * To add handling for all valid monotonic cases with an EXCLUDE + * clause is complex and likely not worth troubling over. For + * now, just bail unless we see EXCLUDE CURRENT ROW with COUNT(*) + * and no FILTER. Excluding the current row is fine when using + * COUNT(*) as this always reduces the count by 1. The same isn't + * true for COUNY(ANY) as a NULL won't be counted, and a + * subsequent non-NULL could make the count decrease. + */ + if ((frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW) == 0 || + wfunc->winfnoid != F_COUNT_ || + wfunc->aggfilter != NULL) + { + req->monotonic = MONOTONICFUNC_NONE; + PG_RETURN_POINTER(req); + } + } + + /* No ORDER BY clause and RANGE mode means all rows are peers. */ + if (req->window_clause->orderClause == NIL && + (frameOptions & FRAMEOPTION_RANGE)) monotonic = MONOTONICFUNC_BOTH; else { diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out index 6ee01f37009..f7bb520402b 100644 --- a/src/test/regress/expected/window.out +++ b/src/test/regress/expected/window.out @@ -4232,23 +4232,59 @@ WHERE c <= 3; (8 rows) -- Ensure we get the correct run condition when the window function is both --- monotonically increasing and decreasing. +-- monotonically increasing and decreasing in RANGE mode without an ORDER BY EXPLAIN (COSTS OFF) SELECT * FROM (SELECT empno, depname, salary, - count(empno) OVER () c + count(empno) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) c FROM empsalary) emp WHERE c = 1; - QUERY PLAN -------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------- WindowAgg - Window: w1 AS () + Window: w1 AS (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) Run Condition: (count(empsalary.empno) OVER w1 = 1) -> Seq Scan on empsalary (4 rows) +-- As above, but check we detect it's monotonically increasing +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + depname, + salary, + count(empno) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +------------------------------------------------------------- + WindowAgg + Window: w1 AS (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) + Run Condition: (count(empsalary.empno) OVER w1 <= 3) + -> Seq Scan on empsalary +(4 rows) + +-- Ensure that ROWS mode without an ORDER BY doesn't think it's monotonically +-- decreasing, i.e. don't push down the run condition. +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + depname, + salary, + count(empno) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) c + FROM empsalary) emp +WHERE c > 1; + QUERY PLAN +------------------------------------------------------------------ + Subquery Scan on emp + Filter: (emp.c > 1) + -> WindowAgg + Window: w1 AS (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) + -> Seq Scan on empsalary +(5 rows) + -- Try another case with a WindowFunc with a byref return type SELECT * FROM (SELECT row_number() OVER (PARTITION BY salary) AS rn, @@ -4436,6 +4472,181 @@ WHERE c = 1; -> Seq Scan on empsalary (9 rows) +-- +-- Ensure we get the correct behavior for run condition pushdown when the +-- frame option has an EXCLUDE clause +-- +-- Ensure pushdown occurs for ROWS BETWEEN UNBOUNDED PRECEDING with EXCLUDE +-- CURRENT ROW with COUNT(*) +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------- + WindowAgg + Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE CURRENT ROW) + Run Condition: (count(*) OVER w1 <= 3) + -> Sort + Sort Key: empsalary.salary + -> Seq Scan on empsalary +(6 rows) + +-- Ensure pushdown occurs for GROUPS BETWEEN UNBOUNDED PRECEDING with EXCLUDE +-- CURRENT ROW with COUNT(*) +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------- + WindowAgg + Window: w1 AS (ORDER BY empsalary.salary GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) + Run Condition: (count(*) OVER w1 <= 3) + -> Sort + Sort Key: empsalary.salary + -> Seq Scan on empsalary +(6 rows) + +-- Ensure pushdown occurs for RANGE BETWEEN UNBOUNDED PRECEDING with EXCLUDE +-- CURRENT ROW with COUNT(*) +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------- + WindowAgg + Window: w1 AS (ORDER BY empsalary.salary RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) + Run Condition: (count(*) OVER w1 <= 3) + -> Sort + Sort Key: empsalary.salary + -> Seq Scan on empsalary +(6 rows) + +-- Ensure we don't get pushdown when a FILTER clause is present +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) FILTER (WHERE salary > 4000) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on emp + Filter: (emp.c <= 3) + -> WindowAgg + Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE CURRENT ROW) + -> Sort + Sort Key: empsalary.salary + -> Seq Scan on empsalary +(7 rows) + +-- Ensure we don't get pushdown with COUNT(ANY) +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(salary) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on emp + Filter: (emp.c <= 3) + -> WindowAgg + Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE CURRENT ROW) + -> Sort + Sort Key: empsalary.salary + -> Seq Scan on empsalary +(7 rows) + +-- Ensure we don't get pushdown with EXCLUDE GROUP +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE GROUP) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on emp + Filter: (emp.c <= 3) + -> WindowAgg + Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE GROUP) + -> Sort + Sort Key: empsalary.salary + -> Seq Scan on empsalary +(7 rows) + +-- Ensure we don't get pushdown with EXCLUDE TIES +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE TIES) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on emp + Filter: (emp.c <= 3) + -> WindowAgg + Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE TIES) + -> Sort + Sort Key: empsalary.salary + -> Seq Scan on empsalary +(7 rows) + +-- Ensure we don't get pushdown with GROUPS mode and EXCLUDE GROUP +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE GROUP) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------ + Subquery Scan on emp + Filter: (emp.c <= 3) + -> WindowAgg + Window: w1 AS (ORDER BY empsalary.salary GROUPS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE GROUP) + -> Sort + Sort Key: empsalary.salary + -> Seq Scan on empsalary +(7 rows) + +-- Ensure we don't get pushdown with GROUPS mode and EXCLUDE TIES +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE TIES) c + FROM empsalary) emp +WHERE c <= 3; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on emp + Filter: (emp.c <= 3) + -> WindowAgg + Window: w1 AS (ORDER BY empsalary.salary GROUPS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE TIES) + -> Sort + Sort Key: empsalary.salary + -> Seq Scan on empsalary +(7 rows) + -- Test Sort node collapsing EXPLAIN (COSTS OFF) SELECT * FROM diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql index ff58f45ce26..cbc11c704ab 100644 --- a/src/test/regress/sql/window.sql +++ b/src/test/regress/sql/window.sql @@ -1361,16 +1361,37 @@ SELECT * FROM WHERE c <= 3; -- Ensure we get the correct run condition when the window function is both --- monotonically increasing and decreasing. +-- monotonically increasing and decreasing in RANGE mode without an ORDER BY EXPLAIN (COSTS OFF) SELECT * FROM (SELECT empno, depname, salary, - count(empno) OVER () c + count(empno) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) c FROM empsalary) emp WHERE c = 1; +-- As above, but check we detect it's monotonically increasing +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + depname, + salary, + count(empno) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + +-- Ensure that ROWS mode without an ORDER BY doesn't think it's monotonically +-- decreasing, i.e. don't push down the run condition. +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + depname, + salary, + count(empno) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) c + FROM empsalary) emp +WHERE c > 1; + -- Try another case with a WindowFunc with a byref return type SELECT * FROM (SELECT row_number() OVER (PARTITION BY salary) AS rn, @@ -1460,6 +1481,96 @@ SELECT * FROM FROM empsalary) emp WHERE c = 1; +-- +-- Ensure we get the correct behavior for run condition pushdown when the +-- frame option has an EXCLUDE clause +-- + +-- Ensure pushdown occurs for ROWS BETWEEN UNBOUNDED PRECEDING with EXCLUDE +-- CURRENT ROW with COUNT(*) +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + +-- Ensure pushdown occurs for GROUPS BETWEEN UNBOUNDED PRECEDING with EXCLUDE +-- CURRENT ROW with COUNT(*) +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + +-- Ensure pushdown occurs for RANGE BETWEEN UNBOUNDED PRECEDING with EXCLUDE +-- CURRENT ROW with COUNT(*) +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + +-- Ensure we don't get pushdown when a FILTER clause is present +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) FILTER (WHERE salary > 4000) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + +-- Ensure we don't get pushdown with COUNT(ANY) +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(salary) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c + FROM empsalary) emp +WHERE c <= 3; + +-- Ensure we don't get pushdown with EXCLUDE GROUP +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE GROUP) c + FROM empsalary) emp +WHERE c <= 3; + +-- Ensure we don't get pushdown with EXCLUDE TIES +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE TIES) c + FROM empsalary) emp +WHERE c <= 3; + +-- Ensure we don't get pushdown with GROUPS mode and EXCLUDE GROUP +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE GROUP) c + FROM empsalary) emp +WHERE c <= 3; + +-- Ensure we don't get pushdown with GROUPS mode and EXCLUDE TIES +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, + salary, + count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE TIES) c + FROM empsalary) emp +WHERE c <= 3; + + -- Test Sort node collapsing EXPLAIN (COSTS OFF) SELECT * FROM From 50313f8f015efd8d4f2de89e4d7fa83a8205aa6d Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Tue, 7 Jul 2026 18:11:28 +0300 Subject: [PATCH 127/250] pg_dump: check for _beginthreadex() failure in parallel dump ParallelBackupStart() stored _beginthreadex()'s return value as the worker's thread handle without checking it. On failure that value is 0, which would later reach WaitForMultipleObjects() as a null handle, caught only by an Assert. The fork() path already calls pg_fatal() when it fails; do the same for _beginthreadex(), as pgbench does. Author: Bryan Green Discussion: https://www.postgresql.org/message-id/8c712d76-ecf7-4749-a6d8-dddc01f298ec@gmail.com Backpatch-through: 14 --- src/bin/pg_dump/parallel.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bin/pg_dump/parallel.c b/src/bin/pg_dump/parallel.c index 086adcdc502..8f57b62ba25 100644 --- a/src/bin/pg_dump/parallel.c +++ b/src/bin/pg_dump/parallel.c @@ -976,6 +976,8 @@ ParallelBackupStart(ArchiveHandle *AH) handle = _beginthreadex(NULL, 0, (void *) &init_spawned_worker_win32, wi, 0, &(slot->threadId)); + if (handle == 0) + pg_fatal("could not create worker thread: %m"); slot->hThread = handle; slot->workerStatus = WRKR_IDLE; #else /* !WIN32 */ From 2167302b748fb8fa9112710014b50910b19b9f15 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Tue, 7 Jul 2026 18:45:34 +0300 Subject: [PATCH 128/250] libpq: Extend "read pending" check from SSL to GSS An extra check for pending bytes in the SSL layer has been part of pqReadReady() for a very long time (79ff2e96d). But when GSS transport encryption was added, it didn't receive the same treatment. (As 79ff2e96d notes, "The bug that I fixed in this patch is exceptionally hard to reproduce reliably.") Without that check, it's possible to hit a hang in gssencmode, if the server splits a large libpq message such that the final message in a streamed response is part of the same wrapped token as the split message: DataRowDataRowDataRowDataRowDataRowData -- token boundary -- RowDataRowCommandCompleteReadyForQuery If the split message takes up enough memory to nearly fill libpq's receive buffer, libpq may return from pqReadData() before the later messages are pulled out of the PqGSSRecvBuffer. Without additional socket activity from the server, pqReadReady() (via pqSocketCheck()) will never again return true, hanging the connection. Pull the pending-bytes check into the pqsecure API layer, where both SSL and GSS now implement it. Note that this does not fix the root problem! Third party clients of libpq have no way to call pqsecure_read_is_pending() in their own polling. This just brings the GSS implementation up to par with the existing SSL workaround; a broader fix is left to a subsequent commit. In preparation for the broader fix, this patch already changes the *_read_pending() functions to return the number of bytes in the buffer rather than just a boolean. The current callers don't need that, but the subsequent fix will. Author: Jacob Champion Discussion: https://postgr.es/m/CAOYmi%2BmpymrgZ76Jre2dx_PwRniS9YZojwH0rZnTuiGHCsj0rA%40mail.gmail.com Backpatch-through: 14 --- src/interfaces/libpq/fe-misc.c | 6 ++--- src/interfaces/libpq/fe-secure-gssapi.c | 7 +++++ src/interfaces/libpq/fe-secure-openssl.c | 34 +++++++++++++++++++++--- src/interfaces/libpq/fe-secure.c | 22 +++++++++++++++ src/interfaces/libpq/libpq-int.h | 6 +++-- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/interfaces/libpq/fe-misc.c b/src/interfaces/libpq/fe-misc.c index dca44fdc5d2..03a4efc8718 100644 --- a/src/interfaces/libpq/fe-misc.c +++ b/src/interfaces/libpq/fe-misc.c @@ -1099,14 +1099,12 @@ pqSocketCheck(PGconn *conn, int forRead, int forWrite, pg_usec_time_t end_time) return -1; } -#ifdef USE_SSL - /* Check for SSL library buffering read bytes */ - if (forRead && conn->ssl_in_use && pgtls_read_pending(conn)) + /* Check for SSL/GSS library buffering read bytes */ + if (forRead && pqsecure_bytes_pending(conn) != 0) { /* short-circuit the select */ return 1; } -#endif } /* We will retry as long as we get EINTR */ diff --git a/src/interfaces/libpq/fe-secure-gssapi.c b/src/interfaces/libpq/fe-secure-gssapi.c index 843b31e175f..05abdbffcc6 100644 --- a/src/interfaces/libpq/fe-secure-gssapi.c +++ b/src/interfaces/libpq/fe-secure-gssapi.c @@ -471,6 +471,13 @@ gss_read(PGconn *conn, void *recv_buffer, size_t length, ssize_t *ret) return PGRES_POLLING_OK; } +ssize_t +pg_GSS_bytes_pending(PGconn *conn) +{ + Assert(PqGSSResultLength >= PqGSSResultNext); + return (ssize_t) (PqGSSResultLength - PqGSSResultNext); +} + /* * Negotiate GSSAPI transport for a connection. When complete, returns * PGRES_POLLING_OK. Will return PGRES_POLLING_READING or diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index 1dd9ba2f506..5f07a3ec2f7 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -230,10 +230,38 @@ pgtls_read(PGconn *conn, void *ptr, size_t len) return n; } -bool -pgtls_read_pending(PGconn *conn) +ssize_t +pgtls_bytes_pending(PGconn *conn) { - return SSL_pending(conn->ssl) > 0; + int pending; + + /* + * OpenSSL readahead is documented to break SSL_pending(). + */ + Assert(!SSL_get_read_ahead(conn->ssl)); + + pending = SSL_pending(conn->ssl); + if (pending < 0) + { + /* shouldn't be possible */ + Assert(false); + libpq_append_conn_error(conn, "OpenSSL reports negative bytes pending"); + return -1; + } + else if (pending == INT_MAX) + { + /* + * If we ever found a legitimate way to hit this, we'd need to loop + * around in the caller to call pgtls_bytes_pending() again. Throw an + * error rather than complicate the code in that way, because + * SSL_read() should be bounded to the size of a single TLS record, + * and conn->inBuffer can't currently go past INT_MAX in size anyway. + */ + libpq_append_conn_error(conn, "OpenSSL reports INT_MAX bytes pending"); + return -1; + } + + return (ssize_t) pending; } ssize_t diff --git a/src/interfaces/libpq/fe-secure.c b/src/interfaces/libpq/fe-secure.c index e686681ba15..64b413e7078 100644 --- a/src/interfaces/libpq/fe-secure.c +++ b/src/interfaces/libpq/fe-secure.c @@ -243,6 +243,28 @@ pqsecure_raw_read(PGconn *conn, void *ptr, size_t len) return n; } +/* + * Return the number of bytes available in the transport buffer. + * + * If pqsecure_read() is called for this number of bytes, it's guaranteed to + * return successfully without reading from the underlying socket. + */ +ssize_t +pqsecure_bytes_pending(PGconn *conn) +{ +#ifdef USE_SSL + if (conn->ssl_in_use) + return pgtls_bytes_pending(conn); +#endif +#ifdef ENABLE_GSS + if (conn->gssenc) + return pg_GSS_bytes_pending(conn); +#endif + + /* Plaintext connections have no transport buffer. */ + return 0; +} + /* * Write data to a secure connection. * diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index c1a72952ff3..3d2a9f671f3 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -821,6 +821,7 @@ extern int pqWriteReady(PGconn *conn); extern PostgresPollingStatusType pqsecure_open_client(PGconn *); extern void pqsecure_close(PGconn *); extern ssize_t pqsecure_read(PGconn *, void *ptr, size_t len); +extern ssize_t pqsecure_bytes_pending(PGconn *); extern ssize_t pqsecure_write(PGconn *, const void *ptr, size_t len); extern ssize_t pqsecure_raw_read(PGconn *, void *ptr, size_t len); extern ssize_t pqsecure_raw_write(PGconn *, const void *ptr, size_t len); @@ -857,9 +858,9 @@ extern void pgtls_close(PGconn *conn); extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len); /* - * Is there unread data waiting in the SSL read buffer? + * Return the number of bytes available in the transport buffer. */ -extern bool pgtls_read_pending(PGconn *conn); +extern ssize_t pgtls_bytes_pending(PGconn *conn); /* * Write data to a secure connection. @@ -907,6 +908,7 @@ extern PostgresPollingStatusType pqsecure_open_gss(PGconn *conn); */ extern ssize_t pg_GSS_write(PGconn *conn, const void *ptr, size_t len); extern ssize_t pg_GSS_read(PGconn *conn, void *ptr, size_t len); +extern ssize_t pg_GSS_bytes_pending(PGconn *conn); #endif /* === in fe-trace.c === */ From bb0a54518176cfb513a9df4a9cf69ba189701fe6 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Tue, 7 Jul 2026 18:45:37 +0300 Subject: [PATCH 129/250] libpq: Drain all pending bytes from SSL/GSS during pqReadData() The previous commit strengthened a workaround for a hang when large messages are split across TLS records/GSS tokens. Because that workaround is implemented in libpq internals, it can only help us when libpq itself is polling on the socket. In nonblocking situations, where the client above libpq is expected to poll, the same bugs can show up. As a contrived example, consider a large protocol-2.0 error coming back from a server during PQconnectPoll(), split in an odd way across two records: -- TLS record (8192-byte payload) -- EEEE[...repeated a total of 8192 times] -- TLS record (8193-byte payload) -- EEEE[...repeated a total of 8192 times]\0 The first record will fill the first half of the libpq receive buffer, which is 16k long by default. The second record completely fills the last half with its first 8192 bytes, leaving the terminating NULL in the OpenSSL buffer. Since we still haven't seen the terminator at our level, PQconnectPoll() will return PGRES_POLLING_READING, expecting to come back when the server has sent "the rest" of the data. But there is nothing left to read from the socket; OpenSSL had to pull all of the data in the 8193-byte record off of the wire to decrypt it. A real server would probably not split up the records this way, nor keep the connection open after sending a fatal connection error. But servers that regularly use larger TLS records can get the libpq receive buffer into the same state if DataRows are big enough, as reported on the list. While the PostgreSQL server doesn't use larger TLS records like that, other non-PostgreSQL servers that implement the wire protocol are known to do that, as well as proxies that sit between the server and the client This is a layering violation. libpq makes decisions based on data in the application buffer, above the transport buffer (whether SSL or GSS), but clients are polling the socket below the transport buffer. One way to fix this in a backportable way, without changing APIs too much, is to ensure data never stays in the transport buffer. Then pqReadData's postconditions will look similar for both raw sockets and SSL/GSS: any available data is either in the application buffer, or still on the socket. Building on the prior commit, make pqReadData() to drain all pending data from the transport layer into conn->inBuffer, expanding the buffer as necessary. This is not particularly efficient from an architectural perspective (the pqsecure_read() implementations take care to fit their packets into the current buffer, and that effort is now completely discarded), but it's hopefully easier to reason about than a full rewrite would be for the back branches. Author: Jacob Champion Reviewed-by: Mark Dilger Reviewed-by: solai v Reported-by: Lars Kanis Discussion: https://postgr.es/m/2039ac58-d3e0-434b-ac1a-2a987f3b4cb1%40greiz-reinsdorf.de Backpatch-through: 14 --- src/interfaces/libpq/fe-misc.c | 145 ++++++++++++++++++++++- src/interfaces/libpq/fe-secure-openssl.c | 4 +- src/interfaces/libpq/fe-secure.c | 3 +- 3 files changed, 148 insertions(+), 4 deletions(-) diff --git a/src/interfaces/libpq/fe-misc.c b/src/interfaces/libpq/fe-misc.c index 03a4efc8718..06cc57863bd 100644 --- a/src/interfaces/libpq/fe-misc.c +++ b/src/interfaces/libpq/fe-misc.c @@ -55,6 +55,8 @@ static int pqPutMsgBytes(const void *buf, size_t len, PGconn *conn); static int pqSendSome(PGconn *conn, int len); static int pqSocketCheck(PGconn *conn, int forRead, int forWrite, pg_usec_time_t end_time); +static int pqReadData_internal(PGconn *conn); +static int pqDrainPending(PGconn *conn); /* * PQlibVersion: return the libpq version number @@ -593,6 +595,13 @@ pqPutMsgEnd(PGconn *conn) /* ---------- * pqReadData: read more data, if any is available + * + * Upon a successful return, callers may assume that either 1) all available + * bytes have been consumed from the socket, or 2) the socket is still marked + * readable by the OS. (In other words: after a successful pqReadData, it's + * safe to tell a client to poll for readable bytes on the socket without any + * further draining of the SSL/GSS transport buffers.) + * * Possible return values: * 1: successfully loaded at least one more byte * 0: no data is presently available, but no error detected @@ -605,8 +614,7 @@ pqPutMsgEnd(PGconn *conn) int pqReadData(PGconn *conn) { - int someread = 0; - int nread; + int available; if (conn->sock == PGINVALID_SOCKET) { @@ -614,6 +622,40 @@ pqReadData(PGconn *conn) return -1; } + available = pqReadData_internal(conn); + if (available < 0) + return -1; + else if (available > 0) + { + /* + * Make sure there are no bytes stuck in layers between conn->inBuffer + * and the socket, to make it safe for clients to poll on PQsocket(). + */ + if (pqDrainPending(conn)) + return -1; + } + else + { + /* + * If we're not returning any bytes from the underlying transport, + * that must imply there aren't any in the transport buffer... + */ + Assert(pqsecure_bytes_pending(conn) == 0); + } + + return available; +} + +/* + * Workhorse for pqReadData(). It's kept separate from the pqDrainPending() + * logic to avoid adding to this function's goto complexity. + */ +static int +pqReadData_internal(PGconn *conn) +{ + int someread = 0; + int nread; + /* Left-justify any data in the buffer to make room */ if (conn->inStart < conn->inEnd) { @@ -800,6 +842,105 @@ pqReadData(PGconn *conn) return -1; } +/*--- + * Drain any transport data that is already buffered in userspace and add it + * to conn->inBuffer, enlarging inBuffer if necessary. The drain fails if + * inBuffer cannot be made to hold all available transport data. + * + * We assume that the underlying secure transport implementation does not + * attempt to read any more data from the socket while draining the transport + * buffer. After a successful return, pqsecure_bytes_pending() must be zero. + * + * This operation is necessary to prevent deadlock, due to a layering + * violation designed into our asynchronous client API: pqReadData() and all + * the parsing routines above it receive data from the SSL/GSS transport + * buffer, but clients poll on the raw PQsocket() handle. So data can be + * "lost" in the intermediate layer if we don't take it out here. + * + * To illustrate what we're trying to prevent, say that the server is sending + * two messages at once in response to a query (Aaaa and Bb), the libpq buffer + * is five characters in size, and TLS records max out at three-character + * payloads. Here's what would happen if pqReadData() didn't call + * pqDrainPending(): + * + * Client libpq SSL Socket + * | | | | + * | [ ] [ ] [ ] [1] Buffers are empty, client is + * x --------------------------> | polling on socket + * | | | | + * | [ ] [ ] [xxx] [2] First record is received; poll + * | <-------------------------- | signals read-ready + * | | | | + * x ---> [ ] [ ] [xxx] [3] Client calls PQconsumeInput() + * | | | | + * | [ ] -> [ ] [xxx] [4] libpq calls pqReadData() to fill + * | | | | the receive buffer + * | [ ] [Aaa] <-- [ ] [5] SSL pulls payload off the wire + * | | | | and decrypts it + * | [Aaa ] <- [ ] [ ] [6] pqsecure_read() takes all data + * | | | | + * | <--- [Aaa ] [ ] [ ] [7] PQconsumeInput() returns with a + * x --------------------------> | partial message, PQisBusy() is + * | | | | still true, client polls again + * | [Aaa ] [ ] [xxx] [8] Second record is received; poll + * | <-------------------------- | signals read-ready + * | | | | + * x ---> [Aaa ] [ ] [xxx] [9] Client calls PQconsumeInput() + * | | | | + * | [Aaa ] -> [ ] [xxx] [10] libpq calls pqReadData() to fill + * | | | | the receive buffer + * | [Aaa ] [aBb] <-- [ ] [11] SSL decrypts + * | | | | + * | [AaaaB] <- [b ] [ ] [12] pqsecure_read() fills its + * | | | | buffer, taking only two bytes + * | <--- [AaaaB] [b ] [ ] [13] PQconsumeInput() returns with a + * | | | | complete message buffered; + * | | | | PQisBusy() is false + * x ---> [AaaaB] [b ] [ ] [14] Client calls PQgetResult() + * | | | | + * | <--- [B ] [b ] [ ] [15] Aaaa is returned; PQisBusy() is + * x --------------------------> | true and client polls again + * . | | . + * . [B ] [b ] . [16] No packets, and client hangs. + * . | | . + * + * The pqDrainPending() call fixes the above scenario at step [13]. Before + * returning to the Client, it first expands the libpq buffer and moves the + * remaining data from the SSL buffer to the libpq buffer. + * + * The function returns 0 on success and -1 on error. Success means that + * there was no data pending or it was successfully drained to conn->inBuffer. + * On error, conn->errorMessage is set. + */ +static int +pqDrainPending(PGconn *conn) +{ + ssize_t bytes_pending; + ssize_t nread; + + bytes_pending = pqsecure_bytes_pending(conn); + if (bytes_pending <= 0) + return bytes_pending; + + /* Expand the input buffer if necessary. */ + if (pqCheckInBufferSpace(conn->inEnd + (size_t) bytes_pending, conn)) + return -1; /* errorMessage already set */ + + nread = pqsecure_read(conn, conn->inBuffer + conn->inEnd, + bytes_pending); + conn->inEnd += nread; + + /* When there are bytes pending, the read function is not supposed to fail */ + if (nread != bytes_pending) + { + libpq_append_conn_error(conn, + "drained only %zu of %zd pending bytes in transport buffer", + nread, bytes_pending); + return -1; + } + return 0; +} + /* * pqSendSome: send data waiting in the output buffer. * diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index 5f07a3ec2f7..98ad05f23cb 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -236,7 +236,9 @@ pgtls_bytes_pending(PGconn *conn) int pending; /* - * OpenSSL readahead is documented to break SSL_pending(). + * OpenSSL readahead is documented to break SSL_pending(). Plus, we can't + * afford to have OpenSSL take bytes off the socket without processing + * them; that breaks the postconditions for pqsecure_drain_pending(). */ Assert(!SSL_get_read_ahead(conn->ssl)); diff --git a/src/interfaces/libpq/fe-secure.c b/src/interfaces/libpq/fe-secure.c index 64b413e7078..e4d9528ac1e 100644 --- a/src/interfaces/libpq/fe-secure.c +++ b/src/interfaces/libpq/fe-secure.c @@ -247,7 +247,8 @@ pqsecure_raw_read(PGconn *conn, void *ptr, size_t len) * Return the number of bytes available in the transport buffer. * * If pqsecure_read() is called for this number of bytes, it's guaranteed to - * return successfully without reading from the underlying socket. + * return successfully without reading from the underlying socket. See + * pqDrainPending() for a more complete discussion of the concepts involved. */ ssize_t pqsecure_bytes_pending(PGconn *conn) From 9021c8f3cabc42839329e3ab0dfed4137557d19f Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Tue, 7 Jul 2026 13:35:15 -0700 Subject: [PATCH 130/250] unicode_case.c: defend against truncated UTF8. Reviewed-by: Chao Li Discussion: https://postgr.es/m/c355354e6c3f4a7aafb047361b73db247260fca0.camel@j-davis.com Backpatch-through: 17 --- src/backend/utils/adt/pg_locale_builtin.c | 24 +++++++--- src/common/unicode/case_test.c | 8 ++++ src/common/unicode_case.c | 55 +++++++++++++++++++---- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/src/backend/utils/adt/pg_locale_builtin.c b/src/backend/utils/adt/pg_locale_builtin.c index df5612b5a2d..faf71d129d7 100644 --- a/src/backend/utils/adt/pg_locale_builtin.c +++ b/src/backend/utils/adt/pg_locale_builtin.c @@ -57,21 +57,33 @@ initcap_wbnext(void *state) while (wbstate->offset < wbstate->len && wbstate->str[wbstate->offset] != '\0') { - pg_wchar u = utf8_to_unicode((unsigned char *) wbstate->str + + int ulen = pg_utf_mblen((const unsigned char *) wbstate->str + wbstate->offset); - bool curr_alnum = pg_u_isalnum(u, wbstate->posix); + pg_wchar u; + bool curr_alnum; + size_t prev_offset = wbstate->offset; - if (!wbstate->init || curr_alnum != wbstate->prev_alnum) + /* invalid UTF8 */ + if (wbstate->offset + ulen > wbstate->len) { - size_t prev_offset = wbstate->offset; + wbstate->init = true; + wbstate->offset = wbstate->len; + return prev_offset; + } + u = utf8_to_unicode((const unsigned char *) wbstate->str + + wbstate->offset); + curr_alnum = pg_u_isalnum(u, wbstate->posix); + + if (!wbstate->init || curr_alnum != wbstate->prev_alnum) + { wbstate->init = true; - wbstate->offset += unicode_utf8len(u); + wbstate->offset += ulen; wbstate->prev_alnum = curr_alnum; return prev_offset; } - wbstate->offset += unicode_utf8len(u); + wbstate->offset += ulen; } return wbstate->len; diff --git a/src/common/unicode/case_test.c b/src/common/unicode/case_test.c index 6dfc4d130ea..d98384e6141 100644 --- a/src/common/unicode/case_test.c +++ b/src/common/unicode/case_test.c @@ -330,6 +330,8 @@ tfunc_fold(char *dst, size_t dstsize, const char *src, static void test_convert_case() { + size_t needed; + /* test string with no case changes */ test_convert(tfunc_lower, "√∞", "√∞"); /* test adjust-to-cased behavior */ @@ -354,6 +356,12 @@ test_convert_case() /* U+FF11 FULLWIDTH ONE is alphanumeric for full case mapping */ test_convert(tfunc_title, "\uFF11a", "\uFF11a"); + /* invalid UTF8: truncated multibyte sequence */ + needed = unicode_strfold(NULL, 0, "abc\xCE", 4, false); + Assert(needed == 3); + /* invalid UTF8: invalid byte */ + needed = unicode_strfold(NULL, 0, "abc\xF8xyz", 7, false); + Assert(needed == 3); #ifdef USE_ICU icu_test_full(""); diff --git a/src/common/unicode_case.c b/src/common/unicode_case.c index 073faf6a0d5..448a4f9d08a 100644 --- a/src/common/unicode_case.c +++ b/src/common/unicode_case.c @@ -193,6 +193,22 @@ unicode_strfold(char *dst, size_t dstsize, const char *src, ssize_t srclen, NULL); } +/* local version of pg_utf_mblen() to be inlinable */ +static int +utf8_mblen(const unsigned char *s) +{ + if ((*s & 0x80) == 0) + return 1; + else if ((*s & 0xe0) == 0xc0) + return 2; + else if ((*s & 0xf0) == 0xe0) + return 3; + else if ((*s & 0xf8) == 0xf0) + return 4; + else + return -1; +} + /* * Implement Unicode Default Case Conversion algorithm. * @@ -229,14 +245,21 @@ convert_case(char *dst, size_t dstsize, const char *src, ssize_t srclen, Assert(boundary == 0); /* start of text is always a boundary */ } - while ((srclen < 0 || srcoff < srclen) && src[srcoff] != '\0') + srclen = (srclen < 0) ? strlen(src) : srclen; + while (srcoff < srclen && src[srcoff] != '\0') { - pg_wchar u1 = utf8_to_unicode((unsigned char *) src + srcoff); - int u1len = unicode_utf8len(u1); + int u1len = utf8_mblen((const unsigned char *) src + srcoff); + pg_wchar u1; pg_wchar simple = 0; const pg_wchar *special = NULL; enum CaseMapResult casemap_result; + /* invalid UTF8 */ + if (u1len < 0 || srcoff + u1len > srclen) + break; + + u1 = utf8_to_unicode((const unsigned char *) src + srcoff); + if (str_casekind == CaseTitle) { if (srcoff == boundary) @@ -320,7 +343,14 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset) { if ((str[i] & 0x80) == 0 || (str[i] & 0xC0) == 0xC0) { - pg_wchar curr = utf8_to_unicode(str + i); + int u1len = utf8_mblen((const unsigned char *) str + i); + pg_wchar curr; + + /* invalid UTF8 */ + if (u1len < 0 || i + u1len > len) + return false; + + curr = utf8_to_unicode(str + i); if (pg_u_prop_case_ignorable(curr)) continue; @@ -331,8 +361,8 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset) } else if ((str[i] & 0xC0) == 0x80) continue; - - Assert(false); /* invalid UTF-8 */ + else + return false; /* invalid UTF8 */ } /* end of string is not followed by a Cased character */ @@ -344,7 +374,14 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset) { if ((str[i] & 0x80) == 0 || (str[i] & 0xC0) == 0xC0) { - pg_wchar curr = utf8_to_unicode(str + i); + int u1len = utf8_mblen((const unsigned char *) str + i); + pg_wchar curr; + + /* invalid UTF8 */ + if (u1len < 0 || i + u1len > len) + return false; + + curr = utf8_to_unicode(str + i); if (pg_u_prop_case_ignorable(curr)) continue; @@ -355,8 +392,8 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset) } else if ((str[i] & 0xC0) == 0x80) continue; - - Assert(false); /* invalid UTF-8 */ + else + return false; /* invalid UTF8 */ } return true; From 66ec24276b18d111b916e6189b25e5c0a998a1f2 Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Tue, 7 Jul 2026 15:04:31 -0700 Subject: [PATCH 131/250] pg_unicode_fast: fix final sigma logic. If the string is preceded only by Case Ignorable characters, don't consider it to be a final sigma. In the process, refactor so that the preceding and following characters are found first, and then the rule is applied, to improve clarity. Discussion: https://postgr.es/m/c355354e6c3f4a7aafb047361b73db247260fca0.camel@j-davis.com Backpatch-through: 18 --- src/common/unicode_case.c | 88 ++++++++++------------ src/test/regress/expected/collate.utf8.out | 6 ++ src/test/regress/sql/collate.utf8.sql | 1 + 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/common/unicode_case.c b/src/common/unicode_case.c index 448a4f9d08a..8639b203e0c 100644 --- a/src/common/unicode_case.c +++ b/src/common/unicode_case.c @@ -328,75 +328,67 @@ convert_case(char *dst, size_t dstsize, const char *src, ssize_t srclen, * 3-17. The character at the given offset must be directly preceded by a * Cased character, and must not be directly followed by a Cased character. * - * Case_Ignorable characters are ignored. NB: some characters may be both + * Case_Ignorable characters are ignored. Neither beginning of string nor end + * of string are considered Cased characters. NB: some characters may be both * Cased and Case_Ignorable, in which case they are ignored. */ static bool check_final_sigma(const unsigned char *str, size_t len, size_t offset) { - /* the start of the string is not preceded by a Cased character */ - if (offset == 0) - return false; + bool preceded_by_cased = false; + bool followed_by_cased = false; + pg_wchar curr; + int ulen; - /* iterate backwards, looking for Cased character */ - for (int i = offset - 1; i >= 0; i--) + /* iterate backwards looking for preceding character */ + for (int i = offset; i > 0;) { - if ((str[i] & 0x80) == 0 || (str[i] & 0xC0) == 0xC0) - { - int u1len = utf8_mblen((const unsigned char *) str + i); - pg_wchar curr; + /* skip backwards through continuation bytes */ + i--; + if ((str[i] & 0xC0) == 0x80) + continue; - /* invalid UTF8 */ - if (u1len < 0 || i + u1len > len) - return false; + /* now at leading byte of previous sequence */ + Assert((str[i] & 0x80) == 0 || (str[i] & 0xC0) == 0xC0); - curr = utf8_to_unicode(str + i); + ulen = utf8_mblen((const unsigned char *) str + i); - if (pg_u_prop_case_ignorable(curr)) - continue; - else if (pg_u_prop_cased(curr)) - break; - else - return false; + /* invalid UTF8 */ + if (ulen < 0 || i + ulen > len) + return false; + + curr = utf8_to_unicode((const unsigned char *) str + i); + + if (!pg_u_prop_case_ignorable(curr)) + { + preceded_by_cased = pg_u_prop_cased(curr); + break; } - else if ((str[i] & 0xC0) == 0x80) - continue; - else - return false; /* invalid UTF8 */ } - /* end of string is not followed by a Cased character */ - if (offset == len) - return true; + ulen = utf8_mblen((const unsigned char *) str + offset); - /* iterate forwards, looking for Cased character */ - for (int i = offset + 1; i < len && str[i] != '\0'; i++) + /* iterate forward looking for following character */ + for (int i = offset + ulen; i < len;) { - if ((str[i] & 0x80) == 0 || (str[i] & 0xC0) == 0xC0) - { - int u1len = utf8_mblen((const unsigned char *) str + i); - pg_wchar curr; + ulen = utf8_mblen((const unsigned char *) str + i); - /* invalid UTF8 */ - if (u1len < 0 || i + u1len > len) - return false; + /* invalid UTF8 */ + if (ulen < 0 || i + ulen > len) + return false; - curr = utf8_to_unicode(str + i); + curr = utf8_to_unicode((const unsigned char *) str + i); - if (pg_u_prop_case_ignorable(curr)) - continue; - else if (pg_u_prop_cased(curr)) - return false; - else - break; + if (!pg_u_prop_case_ignorable(curr)) + { + followed_by_cased = pg_u_prop_cased(curr); + break; } - else if ((str[i] & 0xC0) == 0x80) - continue; - else - return false; /* invalid UTF8 */ + + i += ulen; } - return true; + return (preceded_by_cased && !followed_by_cased); } /* diff --git a/src/test/regress/expected/collate.utf8.out b/src/test/regress/expected/collate.utf8.out index 0c3ab5c89b2..99fdc111fa4 100644 --- a/src/test/regress/expected/collate.utf8.out +++ b/src/test/regress/expected/collate.utf8.out @@ -263,6 +263,12 @@ SELECT lower('ᾼΣͅΑ' COLLATE PG_UNICODE_FAST); -- 0391 0345 03A3 0345 0391 ᾳσͅα (1 row) +SELECT lower(U&'\0300\03A3' COLLATE PG_UNICODE_FAST); + lower +------- + ̀σ +(1 row) + -- properties SELECT 'xyz' ~ '[[:alnum:]]' COLLATE PG_UNICODE_FAST; ?column? diff --git a/src/test/regress/sql/collate.utf8.sql b/src/test/regress/sql/collate.utf8.sql index d6d14220ab3..22aecee3a60 100644 --- a/src/test/regress/sql/collate.utf8.sql +++ b/src/test/regress/sql/collate.utf8.sql @@ -128,6 +128,7 @@ SELECT lower('0Σ' COLLATE PG_UNICODE_FAST); -- 0030 03A3 SELECT lower('ΑΣΑ' COLLATE PG_UNICODE_FAST); -- 0391 03A3 0391 SELECT lower('ἈΣ̓Α' COLLATE PG_UNICODE_FAST); -- 0391 0343 03A3 0343 0391 SELECT lower('ᾼΣͅΑ' COLLATE PG_UNICODE_FAST); -- 0391 0345 03A3 0345 0391 +SELECT lower(U&'\0300\03A3' COLLATE PG_UNICODE_FAST); -- properties From 45364e49688a2a845c6b43b182da3b5f3d10cff8 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Wed, 8 Jul 2026 08:46:43 +0900 Subject: [PATCH 132/250] Fix EXPLAIN failure when deparsing SQL/JSON aggregates If an expression containing an aggregate is evaluated above the plan node that computes the aggregate, as happens with window functions or with expressions postponed to above the final sort, setrefs.c replaces the Aggref or WindowFunc with a Var referencing the lower node's output. For SQL/JSON aggregates such as JSON_ARRAYAGG and JSON_OBJECTAGG, deparsing the containing JsonConstructorExpr then failed with "invalid JsonConstructorExpr underlying node type", since get_json_agg_constructor() did not expect a Var there. Fix by resolving the Var back to the underlying Aggref or WindowFunc and deparsing the constructor as if the aggregate were computed at the current node. The JsonConstructorExpr retains the RETURNING clause and the ABSENT/NULL ON NULL and WITH UNIQUE options, and the arguments come from the resolved aggregate, so the original JSON aggregate syntax is reproduced in full. This mirrors how get_agg_expr() already looks through such a Var when deparsing a combining aggregate. Reported-by: Thom Brown Author: Richard Guo Discussion: https://postgr.es/m/CAA-aLv5QYTaMOk=Qhv6cgwceeHETZV8YJvWZ_rH+yVZCuchATA@mail.gmail.com Backpatch-through: 16 --- src/backend/utils/adt/ruleutils.c | 32 ++++++++ src/test/regress/expected/sqljson.out | 101 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 41 +++++++++++ 3 files changed, 174 insertions(+) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..e3e23433192 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -503,6 +503,8 @@ static void get_json_agg_constructor(JsonConstructorExpr *ctor, deparse_context *context, const char *funcname, bool is_json_objectagg); +static void get_json_agg_constructor_expr(Node *node, deparse_context *context, + void *callback_arg); static void simple_quote_literal(StringInfo buf, const char *val); static void get_sublink_expr(SubLink *sublink, deparse_context *context); static void get_tablefunc(TableFunc *tf, deparse_context *context, @@ -11781,11 +11783,41 @@ get_json_agg_constructor(JsonConstructorExpr *ctor, deparse_context *context, get_windowfunc_expr_helper((WindowFunc *) ctor->func, context, funcname, options.data, is_json_objectagg); + else if (IsA(ctor->func, Var)) + { + /* + * If the aggregate is computed by a lower plan node, setrefs.c will + * have replaced the Aggref or WindowFunc with a Var referencing that + * node's output. Chase the Var back to it so we can still print the + * original JSON aggregate syntax. This only happens in EXPLAIN. + */ + resolve_special_varno((Node *) ctor->func, context, + get_json_agg_constructor_expr, ctor); + } else elog(ERROR, "invalid JsonConstructorExpr underlying node type: %d", nodeTag(ctor->func)); } +/* + * Deparse a JsonConstructorExpr whose aggregate is computed by a lower plan + * node; resolve_special_varno has located the underlying Aggref/WindowFunc. + */ +static void +get_json_agg_constructor_expr(Node *node, deparse_context *context, + void *callback_arg) +{ + JsonConstructorExpr ctor; + + if (!IsA(node, Aggref) && !IsA(node, WindowFunc)) + elog(ERROR, "JSON aggregate constructor does not point to an Aggref or WindowFunc"); + + /* Flat copy suffices; we only replace func. */ + ctor = *(JsonConstructorExpr *) callback_arg; + ctor.func = (Expr *) node; + get_json_constructor(&ctor, context, false); +} + /* * simple_quote_literal - Format a string as a SQL literal, append to buf */ diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 6b0d8815508..0d2fedb7d83 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1411,3 +1411,104 @@ SELECT JSON_OBJECT('a': JSON_OBJECTAGG('b': 1 RETURNING text) FORMAT JSON); (1 row) DROP FUNCTION volatile_one, stable_one; +-- Test deparsing of JSON aggregates that are computed below a WindowAgg +-- node. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT i % 2 AS g, + JSON_ARRAYAGG(i ORDER BY i RETURNING jsonb) AS ja, + JSON_ARRAYAGG(i ORDER BY i RETURNING text) AS ja_text, + JSON_ARRAYAGG(i ORDER BY i NULL ON NULL RETURNING jsonb) AS ja_null, + JSON_OBJECTAGG(i: i ABSENT ON NULL RETURNING jsonb) AS jo_absent, + JSON_OBJECTAGG(i: i WITH UNIQUE RETURNING jsonb) AS jo_unique, + row_number() OVER (ORDER BY i % 2) AS rn +FROM generate_series(1, 3) i +GROUP BY i % 2; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + WindowAgg + Output: ((i % 2)), JSON_ARRAYAGG(i ORDER BY i RETURNING jsonb), JSON_ARRAYAGG(i ORDER BY i RETURNING text), JSON_ARRAYAGG(i ORDER BY i NULL ON NULL RETURNING jsonb), JSON_OBJECTAGG(i : i ABSENT ON NULL RETURNING jsonb), JSON_OBJECTAGG(i : i WITH UNIQUE KEYS RETURNING jsonb), row_number() OVER w1 + Window: w1 AS (ORDER BY ((i.i % 2)) ROWS UNBOUNDED PRECEDING) + -> GroupAggregate + Output: ((i % 2)), jsonb_agg_strict(i ORDER BY i), json_agg_strict(i ORDER BY i), jsonb_agg(i ORDER BY i), jsonb_object_agg_strict(i, i), jsonb_object_agg_unique(i, i) + Group Key: ((i.i % 2)) + -> Sort + Output: ((i % 2)), i + Sort Key: ((i.i % 2)), i.i + -> Function Scan on pg_catalog.generate_series i + Output: (i % 2), i + Function Call: generate_series(1, 3) +(12 rows) + +SELECT i % 2 AS g, + JSON_ARRAYAGG(i ORDER BY i RETURNING jsonb) AS ja, + JSON_ARRAYAGG(i ORDER BY i RETURNING text) AS ja_text, + JSON_ARRAYAGG(i ORDER BY i NULL ON NULL RETURNING jsonb) AS ja_null, + JSON_OBJECTAGG(i: i ABSENT ON NULL RETURNING jsonb) AS jo_absent, + JSON_OBJECTAGG(i: i WITH UNIQUE RETURNING jsonb) AS jo_unique, + row_number() OVER (ORDER BY i % 2) AS rn +FROM generate_series(1, 3) i +GROUP BY i % 2; + g | ja | ja_text | ja_null | jo_absent | jo_unique | rn +---+--------+---------+---------+------------------+------------------+---- + 0 | [2] | [2] | [2] | {"2": 2} | {"2": 2} | 1 + 1 | [1, 3] | [1, 3] | [1, 3] | {"1": 1, "3": 3} | {"1": 1, "3": 3} | 2 +(2 rows) + +-- The same, but with the JSON aggregate used as a window function that is +-- computed below another WindowAgg node. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT JSON_ARRAYAGG(i NULL ON NULL RETURNING jsonb) OVER (ORDER BY i DESC) AS ja, + row_number() OVER (ORDER BY i) AS rn +FROM generate_series(1, 3) i; + QUERY PLAN +------------------------------------------------------------------------------------------ + WindowAgg + Output: JSON_ARRAYAGG(i NULL ON NULL RETURNING jsonb) OVER w1, row_number() OVER w2, i + Window: w2 AS (ORDER BY i.i ROWS UNBOUNDED PRECEDING) + -> Sort + Output: i, (jsonb_agg(i) OVER w1) + Sort Key: i.i + -> WindowAgg + Output: i, jsonb_agg(i) OVER w1 + Window: w1 AS (ORDER BY i.i) + -> Sort + Output: i + Sort Key: i.i DESC + -> Function Scan on pg_catalog.generate_series i + Output: i + Function Call: generate_series(1, 3) +(15 rows) + +SELECT JSON_ARRAYAGG(i NULL ON NULL RETURNING jsonb) OVER (ORDER BY i DESC) AS ja, + row_number() OVER (ORDER BY i) AS rn +FROM generate_series(1, 3) i; + ja | rn +-----------+---- + [3, 2, 1] | 1 + [3, 2] | 2 + [3] | 3 +(3 rows) + +-- The same, but with the expression containing the JSON aggregate postponed +-- to above the final sort due to being volatile. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT i % 2 AS g, + JSON_ARRAYAGG(i RETURNING text) || random()::text AS ja +FROM generate_series(1, 3) i +GROUP BY i % 2 +ORDER BY count(*); + QUERY PLAN +---------------------------------------------------------------------------------------- + Result + Output: ((i % 2)), (JSON_ARRAYAGG(i RETURNING text) || (random())::text), (count(*)) + -> Sort + Output: ((i % 2)), (count(*)), (json_agg_strict(i)) + Sort Key: (count(*)) + -> HashAggregate + Output: ((i % 2)), count(*), json_agg_strict(i) + Group Key: (i.i % 2) + -> Function Scan on pg_catalog.generate_series i + Output: (i % 2), i + Function Call: generate_series(1, 3) +(11 rows) + diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 0b77deb3b24..a995bfa9153 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -529,3 +529,44 @@ SELECT JSON_OBJECT('a': JSON_OBJECTAGG('b': stable_one() RETURNING text) FORMAT EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('a': JSON_OBJECTAGG('b': 1 RETURNING text) FORMAT JSON); SELECT JSON_OBJECT('a': JSON_OBJECTAGG('b': 1 RETURNING text) FORMAT JSON); DROP FUNCTION volatile_one, stable_one; + +-- Test deparsing of JSON aggregates that are computed below a WindowAgg +-- node. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT i % 2 AS g, + JSON_ARRAYAGG(i ORDER BY i RETURNING jsonb) AS ja, + JSON_ARRAYAGG(i ORDER BY i RETURNING text) AS ja_text, + JSON_ARRAYAGG(i ORDER BY i NULL ON NULL RETURNING jsonb) AS ja_null, + JSON_OBJECTAGG(i: i ABSENT ON NULL RETURNING jsonb) AS jo_absent, + JSON_OBJECTAGG(i: i WITH UNIQUE RETURNING jsonb) AS jo_unique, + row_number() OVER (ORDER BY i % 2) AS rn +FROM generate_series(1, 3) i +GROUP BY i % 2; +SELECT i % 2 AS g, + JSON_ARRAYAGG(i ORDER BY i RETURNING jsonb) AS ja, + JSON_ARRAYAGG(i ORDER BY i RETURNING text) AS ja_text, + JSON_ARRAYAGG(i ORDER BY i NULL ON NULL RETURNING jsonb) AS ja_null, + JSON_OBJECTAGG(i: i ABSENT ON NULL RETURNING jsonb) AS jo_absent, + JSON_OBJECTAGG(i: i WITH UNIQUE RETURNING jsonb) AS jo_unique, + row_number() OVER (ORDER BY i % 2) AS rn +FROM generate_series(1, 3) i +GROUP BY i % 2; + +-- The same, but with the JSON aggregate used as a window function that is +-- computed below another WindowAgg node. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT JSON_ARRAYAGG(i NULL ON NULL RETURNING jsonb) OVER (ORDER BY i DESC) AS ja, + row_number() OVER (ORDER BY i) AS rn +FROM generate_series(1, 3) i; +SELECT JSON_ARRAYAGG(i NULL ON NULL RETURNING jsonb) OVER (ORDER BY i DESC) AS ja, + row_number() OVER (ORDER BY i) AS rn +FROM generate_series(1, 3) i; + +-- The same, but with the expression containing the JSON aggregate postponed +-- to above the final sort due to being volatile. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT i % 2 AS g, + JSON_ARRAYAGG(i RETURNING text) || random()::text AS ja +FROM generate_series(1, 3) i +GROUP BY i % 2 +ORDER BY count(*); From 1383cbd03141316d98329341191d856c65d7c2c2 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 8 Jul 2026 09:04:31 +0900 Subject: [PATCH 133/250] doc: Fix typo in rule-system view example Commit dcb00495236 accidentally changed the final expanded query's condition to > 2 while rewriting the example into SQL operator notation. The original query and the preceding rewritten forms all use >= 2, and view expansion should preserve that qualification. This commit changes the final condition from > 2 to >= 2. Backpatch to all supported versions. Reported-by: Yaroslav Saburov Author: Fujii Masao Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/178248467618.108999.9966122434342474006@wrigleys.postgresql.org Backpatch-through: 14 --- doc/src/sgml/rules.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml index 8467d961fd0..1fc2e84827b 100644 --- a/doc/src/sgml/rules.sgml +++ b/doc/src/sgml/rules.sgml @@ -630,7 +630,7 @@ SELECT shoe_ready.shoename, shoe_ready.sh_avail, WHERE rsl.sl_color = rsh.slcolor AND rsl.sl_len_cm >= rsh.slminlen_cm AND rsl.sl_len_cm <= rsh.slmaxlen_cm) shoe_ready - WHERE shoe_ready.total_avail > 2; + WHERE shoe_ready.total_avail >= 2; From abecdbc6af0192eb8fc017af5c7a3bb3d5da1ae3 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 8 Jul 2026 12:45:20 +0900 Subject: [PATCH 134/250] doc: Clarify COPY FROM WHERE expression restrictions Commit aa606b9316a disallowed generated columns in COPY FROM WHERE expressions, and commit 21c69dc73f9 disallowed system columns. However, the COPY reference page still mentions only the restriction on subqueries. Update the documentation to also list generated columns and system columns as unsupported in COPY FROM WHERE expressions. Backpatch the generated-column documentation change to all supported versions. Backpatch the system-column documentation change to v19, where that restriction was introduced. Author: Fujii Masao Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/CAHGQGwEgxErc54yVOAVWCsr1O=8pgw4oKRPuEQ9mfhkoYGR_XA@mail.gmail.com Backpatch-through: 14 --- doc/src/sgml/ref/copy.sgml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index 8433344e5b6..5413bb0e12d 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -480,10 +480,11 @@ WHERE condition - Currently, subqueries are not allowed in WHERE - expressions, and the evaluation does not see any changes made by the - COPY itself (this matters when the expression - contains calls to VOLATILE functions). + Currently, subqueries and generated columns are not allowed in + WHERE expressions, and the evaluation does not see + any changes made by the COPY itself (this matters + when the expression contains calls to VOLATILE + functions). From 4908225bebdc31c9b8f735ad1db3d08d9bc85148 Mon Sep 17 00:00:00 2001 From: Dean Rasheed Date: Wed, 8 Jul 2026 20:46:26 +0100 Subject: [PATCH 135/250] Fix RETURNING OLD with BEFORE UPDATE trigger and concurrent update. When executing an UPDATE with a RETURNING clause on a table with a BEFORE UPDATE row trigger, the computation of the OLD values in the RETURNING list was incorrect if the target tuple was concurrently updated by another session, at isolation level READ COMMITTED. The problem was that the trigger code would lock the target tuple, waiting for the other session to commit, and then fetch the updated target tuple, but ExecUpdate() would not realise that the target tuple had changed, and use the outdated target tuple for computing OLD values. Fix by having ExecUpdate() check the TM_FailureData from trigger execution and re-fetch the target tuple if necessary. Re-fetching the target tuple like this is a little inefficient, but probably negligible compared to the trigger execution and update. A better long-term fix might be to move the EPQ code out of trigger.c, and let ExecUpdate() handle it, like ExecMergeMatched() does, but that would likely mean changing the trigger API, which seems a bit much for back-patching. Backpatch to v18, where support for RETURNING OLD/NEW was added. Bug: #19536 Reported-by: Jonas Boberg Diagnosed-by: Bharath Rupireddy Author: Dean Rasheed Reviewed-by: Bharath Rupireddy Discussion: https://postgr.es/m/19536-73ce5847e6c0e7b1@postgresql.org Backpatch-through: 18 --- src/backend/executor/nodeModifyTable.c | 19 ++ .../expected/eval-plan-qual-trigger.out | 258 +++++++++--------- .../expected/merge-match-recheck.out | 179 +++++++++++- .../specs/eval-plan-qual-trigger.spec | 8 +- .../isolation/specs/merge-match-recheck.spec | 21 +- 5 files changed, 333 insertions(+), 152 deletions(-) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 7c1d0e9588e..df99a204e0c 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -2477,9 +2477,28 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, * Prepare for the update. This includes BEFORE ROW triggers, so we're * done if it says we are. */ + context->tmfd.traversed = false; if (!ExecUpdatePrologue(context, resultRelInfo, tupleid, oldtuple, slot, NULL)) return NULL; + /* + * If the target tuple was concurrently updated, the trigger code will + * have done EPQ and updated tupleid, following the update chain. In this + * case, we must fetch the most recent version of old tuple for the + * benefit of RETURNING. Technically, we could get away with not doing + * this, if there is no RETURNING clause, or it doesn't refer to OLD, but + * it seems preferable to always ensure that the contents of oldSlot are + * correct. + */ + if (context->tmfd.traversed) + { + if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, + tupleid, + SnapshotAny, + oldSlot)) + elog(ERROR, "failed to re-fetch tuple updated during trigger execution"); + } + /* INSTEAD OF ROW UPDATE Triggers */ if (resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->trig_update_instead_row) diff --git a/src/test/isolation/expected/eval-plan-qual-trigger.out b/src/test/isolation/expected/eval-plan-qual-trigger.out index f6714c2e599..eca8606f60f 100644 --- a/src/test/isolation/expected/eval-plan-qual-trigger.out +++ b/src/test/isolation/expected/eval-plan-qual-trigger.out @@ -61,11 +61,11 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; -key |data ------+------------------ -key-a|val-a-s1-ups1-ups2 +key |data |check_old_and_new +-----+------------------+----------------- +key-a|val-a-s1-ups1-ups2|t (1 row) step s2_c: COMMIT; @@ -132,11 +132,11 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -205,11 +205,11 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; -key |data ------+------------- -key-a|val-a-s1-ups1 +key |data |check_old +-----+-------------+--------- +key-a|val-a-s1-ups1|t (1 row) step s2_c: COMMIT; @@ -277,11 +277,11 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; -key |data ------+-------- -key-a|val-a-s1 +key |data |check_old +-----+--------+--------- +key-a|val-a-s1|t (1 row) step s2_c: COMMIT; @@ -343,7 +343,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -352,9 +352,9 @@ s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (k s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------------ -key-a|val-a-s1-ups1-ups2 +key |data |check_old_and_new +-----+------------------+----------------- +key-a|val-a-s1-ups1-ups2|t (1 row) step s2_c: COMMIT; @@ -417,16 +417,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -491,7 +491,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -500,9 +500,9 @@ s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (k s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------------ -key-a|val-a-s1-ups1-ups2 +key |data |check_old_and_new +-----+------------------+----------------- +key-a|val-a-s1-ups1-ups2|t (1 row) step s2_c: COMMIT; @@ -567,16 +567,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -641,13 +641,13 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-b = text key-a: f step s2_upd_a_data: <... completed> -key|data ----+---- +key|data|check_old_and_new +---+----+----------------- (0 rows) step s2_c: COMMIT; @@ -711,16 +711,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -873,7 +873,7 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -881,9 +881,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) step s2_upsert_a_data: <... completed> -key |data ------+---------------------- -key-a|val-a-s1-ups1-upserts2 +key |data |check_old_and_new +-----+----------------------+----------------- +key-a|val-a-s1-ups1-upserts2|t (1 row) step s2_c: COMMIT; @@ -955,7 +955,7 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -963,9 +963,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) step s2_upsert_a_data: <... completed> -key |data ------+---------------------- -key-a|val-a-s1-ups1-upserts2 +key |data |check_old_and_new +-----+----------------------+----------------- +key-a|val-a-s1-ups1-upserts2|t (1 row) step s2_c: COMMIT; @@ -1012,7 +1012,7 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -1020,9 +1020,9 @@ s2: NOTICE: upk: text val-a-s1 <> text mismatch: t s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-upserts2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-upserts2) step s2_upsert_a_data: <... completed> -key |data ------+----------------- -key-a|val-a-s1-upserts2 +key |data |check_old_and_new +-----+-----------------+----------------- +key-a|val-a-s1-upserts2|t (1 row) step s2_c: COMMIT; @@ -1068,14 +1068,14 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_a_i; when: AFTER; lev: ROWs; op: INSERT; old: new: (key-a,val-a-upss2) step s2_upsert_a_data: <... completed> -key |data ------+----------- -key-a|val-a-upss2 +key |data |check_old_and_new +-----+-----------+----------------- +key-a|val-a-upss2| (1 row) step s2_c: COMMIT; @@ -1137,7 +1137,7 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -1145,9 +1145,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-upserts2) step s2_upsert_a_data: <... completed> -key |data ------+---------------------- -key-a|val-a-s1-ups1-upserts2 +key |data |check_old_and_new +-----+----------------------+----------------- +key-a|val-a-s1-ups1-upserts2|t (1 row) step s2_c: COMMIT; @@ -1209,14 +1209,14 @@ step s2_upsert_a_data: WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_a_i; when: AFTER; lev: ROWs; op: INSERT; old: new: (key-a,val-a-upss2) step s2_upsert_a_data: <... completed> -key |data ------+----------- -key-a|val-a-upss2 +key |data |check_old_and_new +-----+-----------+----------------- +key-a|val-a-upss2| (1 row) step s2_c: COMMIT; @@ -1276,7 +1276,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -1284,9 +1284,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1-ups1) new: (key-a,val-a-s1-ups1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------------ -key-a|val-a-s1-ups1-ups2 +key |data |check_old_and_new +-----+------------------+----------------- +key-a|val-a-s1-ups1-ups2|t (1 row) step s2_c: COMMIT; @@ -1347,15 +1347,15 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -1417,7 +1417,7 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_c: COMMIT; s2: NOTICE: upd: text key-a = text key-a: t @@ -1425,9 +1425,9 @@ s2: NOTICE: upk: text val-a-s1-ups1 <> text mismatch: t s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_d; when: AFTER; lev: ROWs; op: DELETE; old: (key-a,val-a-s1-ups1) new: step s2_del_a: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups1 +key |data |check_old +-----+-------------+--------- +key-a|val-a-s1-ups1|t (1 row) step s2_c: COMMIT; @@ -1488,15 +1488,15 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_r: ROLLBACK; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_d; when: AFTER; lev: ROWs; op: DELETE; old: (key-a,val-a-s1) new: step s2_del_a: <... completed> -key |data ------+-------- -key-a|val-a-s1 +key |data |check_old +-----+--------+--------- +key-a|val-a-s1|t (1 row) step s2_c: COMMIT; @@ -1557,13 +1557,13 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-b = text key-a: f step s2_upd_a_data: <... completed> -key|data ----+---- +key|data|check_old_and_new +---+----+----------------- (0 rows) step s2_c: COMMIT; @@ -1624,15 +1624,15 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -1693,13 +1693,13 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_c: COMMIT; s2: NOTICE: upd: text key-b = text key-a: f step s2_del_a: <... completed> -key|data ----+---- +key|data|check_old +---+----+--------- (0 rows) step s2_c: COMMIT; @@ -1759,15 +1759,15 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_r: ROLLBACK; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_d; when: AFTER; lev: ROWs; op: DELETE; old: (key-a,val-a-s1) new: step s2_del_a: <... completed> -key |data ------+-------- -key-a|val-a-s1 +key |data |check_old +-----+--------+--------- +key-a|val-a-s1|t (1 row) step s2_c: COMMIT; @@ -1829,14 +1829,14 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: upd: text key-c = text key-a: f step s2_upd_a_data: <... completed> -key|data ----+---- +key|data|check_old_and_new +---+----+----------------- (0 rows) step s2_c: COMMIT; @@ -1899,16 +1899,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-c = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -2038,7 +2038,7 @@ step s2_upd_all_data: WHERE noisy_oper('upd', key, '<>', 'mismatch') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-b <> text mismatch: t @@ -2050,10 +2050,10 @@ s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (k s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-b,val-a-s1-tobs1) new: (key-b,val-a-s1-tobs1-ups2) s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-c,val-c-s1) new: (key-c,val-c-s1-ups2) step s2_upd_all_data: <... completed> -key |data ------+------------------- -key-b|val-a-s1-tobs1-ups2 -key-c|val-c-s1-ups2 +key |data |check_old_and_new +-----+-------------------+----------------- +key-b|val-a-s1-tobs1-ups2|t +key-c|val-c-s1-ups2 |t (2 rows) step s2_c: COMMIT; @@ -2118,13 +2118,13 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; s2: NOTICE: upd: text key-c = text key-a: f step s2_upd_a_data: <... completed> -key|data ----+---- +key|data|check_old_and_new +---+----+----------------- (0 rows) step s2_c: COMMIT; @@ -2188,16 +2188,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-c = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -2260,13 +2260,13 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_c: COMMIT; s2: NOTICE: upd: text key-c = text key-a: f step s2_del_a: <... completed> -key|data ----+---- +key|data|check_old +---+----+--------- (0 rows) step s2_c: COMMIT; @@ -2328,16 +2328,16 @@ step s2_del_a: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_d; when: BEFORE; lev: ROWs; op: DELETE; old: (key-a,val-a-s1) new: s2: NOTICE: upd: text key-c = text key-a: f s2: NOTICE: trigger: name rep_a_d; when: AFTER; lev: ROWs; op: DELETE; old: (key-a,val-a-s1) new: step s2_del_a: <... completed> -key |data ------+-------- -key-a|val-a-s1 +key |data |check_old +-----+--------+--------- +key-a|val-a-s1|t (1 row) step s2_c: COMMIT; @@ -2507,7 +2507,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; step s2_upd_a_data: <... completed> @@ -2572,16 +2572,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; @@ -2646,7 +2646,7 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_c: COMMIT; step s2_upd_a_data: <... completed> @@ -2712,16 +2712,16 @@ step s2_upd_a_data: WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; step s1_r: ROLLBACK; s2: NOTICE: trigger: name rep_b_u; when: BEFORE; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) s2: NOTICE: upd: text key-b = text key-a: f s2: NOTICE: trigger: name rep_a_u; when: AFTER; lev: ROWs; op: UPDATE; old: (key-a,val-a-s1) new: (key-a,val-a-s1-ups2) step s2_upd_a_data: <... completed> -key |data ------+------------- -key-a|val-a-s1-ups2 +key |data |check_old_and_new +-----+-------------+----------------- +key-a|val-a-s1-ups2|t (1 row) step s2_c: COMMIT; diff --git a/src/test/isolation/expected/merge-match-recheck.out b/src/test/isolation/expected/merge-match-recheck.out index 4250b85af2d..10ef2ad2fcd 100644 --- a/src/test/isolation/expected/merge-match-recheck.out +++ b/src/test/isolation/expected/merge-match-recheck.out @@ -197,15 +197,23 @@ step merge_bal: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+---------------------------------- + 1| 50| 100|s1 |setup updated by update_bal1 when1 +(1 row) + step select1: SELECT * FROM target; key|balance|status|val ---+-------+------+---------------------------------- @@ -220,15 +228,23 @@ step merge_bal_pa: MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal_pa: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------- + 1| 50| 100|s1 |setup updated by update_bal1_pa when1 +(1 row) + step select1_pa: SELECT * FROM target_pa; key|balance|status|val ---+-------+------+------------------------------------- @@ -245,22 +261,24 @@ step merge_bal_tg: MERGE INTO target_tg t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN UPDATE SET balance = balance * 8, val = t.val || ' when3' - RETURNING t.* + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val ) SELECT * FROM t; step c2: COMMIT; s1: NOTICE: Update: (1,50,s1,"setup updated by update_bal1_tg") -> (1,100,s1,"setup updated by update_bal1_tg when1") step merge_bal_tg: <... completed> -key|balance|status|val ----+-------+------+------------------------------------- - 1| 100|s1 |setup updated by update_bal1_tg when1 +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------- + 1| 50| 100|s1 |setup updated by update_bal1_tg when1 (1 row) step select1_tg: SELECT * FROM target_tg; @@ -278,15 +296,23 @@ step merge_bal: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------- + 1| 70| 140|s1 |setup updated by update1 updated by update6 when1 +(1 row) + step select1: SELECT * FROM target; key|balance|status|val ---+-------+------+------------------------------------------------- @@ -302,15 +328,23 @@ step merge_bal_pa: MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal_pa: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------------- + 1| 70| 140|s1 |setup updated by update1_pa updated by update6_pa when1 +(1 row) + step select1_pa: SELECT * FROM target_pa; key|balance|status|val ---+-------+------+------------------------------------------------------- @@ -329,22 +363,24 @@ step merge_bal_tg: MERGE INTO target_tg t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN UPDATE SET balance = balance * 8, val = t.val || ' when3' - RETURNING t.* + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val ) SELECT * FROM t; step c2: COMMIT; s1: NOTICE: Update: (1,70,s1,"setup updated by update1_tg updated by update6_tg") -> (1,140,s1,"setup updated by update1_tg updated by update6_tg when1") step merge_bal_tg: <... completed> -key|balance|status|val ----+-------+------+------------------------------------------------------- - 1| 140|s1 |setup updated by update1_tg updated by update6_tg when1 +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------------- + 1| 70| 140|s1 |setup updated by update1_tg updated by update6_tg when1 (1 row) step select1_tg: SELECT * FROM target_tg; @@ -355,6 +391,105 @@ key|balance|status|val step c1: COMMIT; +starting permutation: update6 update6 merge_bal c2 select1 c1 +step update6: UPDATE target t SET balance = balance - 100, val = t.val || ' updated by update6' WHERE t.key = 1; +step update6: UPDATE target t SET balance = balance - 100, val = t.val || ' updated by update6' WHERE t.key = 1; +step merge_bal: + MERGE INTO target t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; + +step c2: COMMIT; +step merge_bal: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------- + 1| -40| |s1 |setup updated by update6 updated by update6 +(1 row) + +step select1: SELECT * FROM target; +key|balance|status|val +---+-------+------+--- +(0 rows) + +step c1: COMMIT; + +starting permutation: update6_pa update6_pa merge_bal_pa c2 select1_pa c1 +step update6_pa: UPDATE target_pa t SET balance = balance - 100, val = t.val || ' updated by update6_pa' WHERE t.key = 1; +step update6_pa: UPDATE target_pa t SET balance = balance - 100, val = t.val || ' updated by update6_pa' WHERE t.key = 1; +step merge_bal_pa: + MERGE INTO target_pa t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; + +step c2: COMMIT; +step merge_bal_pa: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------- + 1| -40| |s1 |setup updated by update6_pa updated by update6_pa +(1 row) + +step select1_pa: SELECT * FROM target_pa; +key|balance|status|val +---+-------+------+--- +(0 rows) + +step c1: COMMIT; + +starting permutation: update6_tg update6_tg merge_bal_tg c2 select1_tg c1 +s2: NOTICE: Update: (1,160,s1,setup) -> (1,60,s1,"setup updated by update6_tg") +step update6_tg: UPDATE target_tg t SET balance = balance - 100, val = t.val || ' updated by update6_tg' WHERE t.key = 1; +s2: NOTICE: Update: (1,60,s1,"setup updated by update6_tg") -> (1,-40,s1,"setup updated by update6_tg updated by update6_tg") +step update6_tg: UPDATE target_tg t SET balance = balance - 100, val = t.val || ' updated by update6_tg' WHERE t.key = 1; +step merge_bal_tg: + WITH t AS ( + MERGE INTO target_tg t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val + ) + SELECT * FROM t; + +step c2: COMMIT; +s1: NOTICE: Delete: (1,-40,s1,"setup updated by update6_tg updated by update6_tg") +step merge_bal_tg: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------- + 1| -40| |s1 |setup updated by update6_tg updated by update6_tg +(1 row) + +step select1_tg: SELECT * FROM target_tg; +key|balance|status|val +---+-------+------+--- +(0 rows) + +step c1: COMMIT; + starting permutation: update7 update6 merge_bal c2 select1 c1 step update7: UPDATE target t SET balance = 350, val = t.val || ' updated by update7' WHERE t.key = 1; step update6: UPDATE target t SET balance = balance - 100, val = t.val || ' updated by update6' WHERE t.key = 1; @@ -362,15 +497,23 @@ step merge_bal: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal: <... completed> +key|old_balance|new_balance|status|val +---+-----------+-----------+------+------------------------------------------------- + 1| 250| 2000|s1 |setup updated by update7 updated by update6 when3 +(1 row) + step select1: SELECT * FROM target; key|balance|status|val ---+-------+------+------------------------------------------------- @@ -385,12 +528,15 @@ step merge_bal_pa: MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal_pa: <... completed> @@ -404,12 +550,15 @@ step merge_bal_pa: MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; step c2: COMMIT; step merge_bal_pa: <... completed> diff --git a/src/test/isolation/specs/eval-plan-qual-trigger.spec b/src/test/isolation/specs/eval-plan-qual-trigger.spec index b512edd2879..e7a1d16ae89 100644 --- a/src/test/isolation/specs/eval-plan-qual-trigger.spec +++ b/src/test/isolation/specs/eval-plan-qual-trigger.spec @@ -120,14 +120,14 @@ step s2_del_a { WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING * + RETURNING *, data = old.data AS check_old; } step s2_upd_a_data { UPDATE trigtest SET data = data || '-ups2' WHERE noisy_oper('upd', key, '=', 'key-a') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; } step s2_upd_b_data { UPDATE trigtest SET data = data || '-ups2' @@ -141,7 +141,7 @@ step s2_upd_all_data { WHERE noisy_oper('upd', key, '<>', 'mismatch') AND noisy_oper('upk', data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-ups2' AS check_old_and_new; } step s2_upsert_a_data { INSERT INTO trigtest VALUES ('key-a', 'val-a-upss2') @@ -150,7 +150,7 @@ step s2_upsert_a_data { WHERE noisy_oper('upd', trigtest.key, '=', 'key-a') AND noisy_oper('upk', trigtest.data, '<>', 'mismatch') - RETURNING *; + RETURNING *, new.data = old.data || '-upserts2' AS check_old_and_new; } session s3 diff --git a/src/test/isolation/specs/merge-match-recheck.spec b/src/test/isolation/specs/merge-match-recheck.spec index 6e7a776d17e..6054fcade26 100644 --- a/src/test/isolation/specs/merge-match-recheck.spec +++ b/src/test/isolation/specs/merge-match-recheck.spec @@ -10,7 +10,7 @@ setup INSERT INTO target VALUES (1, 160, 's1', 'setup'); CREATE TABLE target_pa (key int, balance integer, status text, val text) PARTITION BY RANGE (balance); - CREATE TABLE target_pa1 PARTITION OF target_pa FOR VALUES FROM (0) TO (200); + CREATE TABLE target_pa1 PARTITION OF target_pa FOR VALUES FROM (-100) TO (200); CREATE TABLE target_pa2 PARTITION OF target_pa FOR VALUES FROM (200) TO (1000); INSERT INTO target_pa VALUES (1, 160, 's1', 'setup'); @@ -78,24 +78,30 @@ step "merge_bal" MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; } step "merge_bal_pa" { MERGE INTO target_pa t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val; } step "merge_bal_tg" { @@ -103,13 +109,15 @@ step "merge_bal_tg" MERGE INTO target_tg t USING (SELECT 1 as key) s ON s.key = t.key + WHEN MATCHED AND balance < 0 THEN + DELETE WHEN MATCHED AND balance < 100 THEN UPDATE SET balance = balance * 2, val = t.val || ' when1' WHEN MATCHED AND balance < 200 THEN UPDATE SET balance = balance * 4, val = t.val || ' when2' WHEN MATCHED AND balance < 300 THEN UPDATE SET balance = balance * 8, val = t.val || ' when3' - RETURNING t.* + RETURNING t.key, old.balance AS old_balance, new.balance AS new_balance, t.status, t.val ) SELECT * FROM t; } @@ -190,6 +198,11 @@ permutation "update1" "update6" "merge_bal" "c2" "select1" "c1" permutation "update1_pa" "update6_pa" "merge_bal_pa" "c2" "select1_pa" "c1" permutation "update1_tg" "update6_tg" "merge_bal_tg" "c2" "select1_tg" "c1" +# merge_bal sees row concurrently updated twice and rechecks WHEN conditions, different check passes, and row is deleted +permutation "update6" "update6" "merge_bal" "c2" "select1" "c1" +permutation "update6_pa" "update6_pa" "merge_bal_pa" "c2" "select1_pa" "c1" +permutation "update6_tg" "update6_tg" "merge_bal_tg" "c2" "select1_tg" "c1" + # merge_bal sees row concurrently updated twice, first update would cause all checks to fail, second update causes different check to pass, so final balance = 2000 permutation "update7" "update6" "merge_bal" "c2" "select1" "c1" From 27761c0151b3b9b57e58597fa027a26619c9608e Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 9 Jul 2026 18:34:24 +0300 Subject: [PATCH 136/250] ssl: Include limits.h to get INT_MAX when using LibreSSL When compiling against OpenSSL, the header is indirectly included via openssl/ossl_typ.h from openssl/conf.h, but the LibreSSL version of ossl_typ.h does not include which cause compiler failure due to missing symbol (since ffd080d94fe). Fix by explicitly including . Author: Daniel Gustafsson Discussion: https://www.postgresql.org/message-id/6A9E7815-BD5A-4C31-A515-48159823406B@yesql.se Backpatch-through: 14 --- src/interfaces/libpq/fe-secure-openssl.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index 98ad05f23cb..5fe1da4ad38 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -25,6 +25,7 @@ #include #include #include +#include #include "libpq-fe.h" #include "fe-auth.h" From 87c3a79ea206410f34f326eb63c36d8679edeb8b Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 9 Jul 2026 18:34:27 +0300 Subject: [PATCH 137/250] libpq: Make error checks in the new buffer draining code more robust Check explicitly for pqsecure_read() returning an error. It shouldn't fail, and we would've caught it in the check for a short read, but better to be explicit so that the error message is more informative. We also shouldn't update 'inEnd' when the read fails, although that too is just pro forma as we will bail out and close the connection on error. Reported-by: Peter Eisentraut Discussion: https://www.postgresql.org/message-id/34844e8c-267c-4daf-b1e0-f26059a4a7d3@eisentraut.org Backpatch-through: 14 --- src/interfaces/libpq/fe-misc.c | 11 ++++++++--- src/interfaces/libpq/fe-secure.c | 5 +++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/interfaces/libpq/fe-misc.c b/src/interfaces/libpq/fe-misc.c index 06cc57863bd..f93124b1b23 100644 --- a/src/interfaces/libpq/fe-misc.c +++ b/src/interfaces/libpq/fe-misc.c @@ -928,13 +928,18 @@ pqDrainPending(PGconn *conn) nread = pqsecure_read(conn, conn->inBuffer + conn->inEnd, bytes_pending); - conn->inEnd += nread; - /* When there are bytes pending, the read function is not supposed to fail */ + /* + * When there are bytes pending, pqsecure_read() is not supposed to fail + * or do a short read, but let's check anyway to be safe. + */ + if (nread < 0) + return -1; + conn->inEnd += nread; if (nread != bytes_pending) { libpq_append_conn_error(conn, - "drained only %zu of %zd pending bytes in transport buffer", + "drained only %zd of %zd pending bytes in transport buffer", nread, bytes_pending); return -1; } diff --git a/src/interfaces/libpq/fe-secure.c b/src/interfaces/libpq/fe-secure.c index e4d9528ac1e..3e161506a37 100644 --- a/src/interfaces/libpq/fe-secure.c +++ b/src/interfaces/libpq/fe-secure.c @@ -247,8 +247,9 @@ pqsecure_raw_read(PGconn *conn, void *ptr, size_t len) * Return the number of bytes available in the transport buffer. * * If pqsecure_read() is called for this number of bytes, it's guaranteed to - * return successfully without reading from the underlying socket. See - * pqDrainPending() for a more complete discussion of the concepts involved. + * return successfully with the same number of bytes, without reading from the + * underlying socket. See pqDrainPending() for a more complete discussion of + * the concepts involved. */ ssize_t pqsecure_bytes_pending(PGconn *conn) From d1d9688b1f92590110cb868ce4f65f39da0af2a8 Mon Sep 17 00:00:00 2001 From: Tomas Vondra Date: Sat, 11 Jul 2026 15:14:50 +0200 Subject: [PATCH 138/250] Shorten pg_attribute_always_inline to pg_always_inline The pg_attribute_always_inline macro name is so long it forces pgindent to format the code in strange ways. Which may incentivize patch authors to either structure the code in strange ways (e.g. reorder prototypes), use shorter names, etc. Neither is very desirable for code readability. This shortens the name by removing the _attribute_ part. It also makes it more consistent with pg_noinline, which does not have the _attribute_ part either. Backpatched to all supported branches, to prevent conflicts when backpatching other fixes. The backbranches however keep both the old and new macro name, so that existing code keeps working. Author: Andres Freund Reviewed-by: Peter Geoghegan Reviewed-by: Tomas Vondra Discussion: https://postgr.es/m/bqqdehahpoa36igpictuqyn2s2mexk3t3ehidh2ffd2slb35e5@rzgksuiszgbg Backpatch-through: 14 --- src/backend/access/heap/heapam.c | 2 +- src/backend/access/transam/xlog.c | 4 +- src/backend/commands/copyfromparse.c | 26 ++++++------- src/backend/commands/copyto.c | 4 +- src/backend/executor/execExprInterp.c | 54 +++++++++++++-------------- src/backend/executor/execTuples.c | 6 +-- src/backend/executor/nodeHashjoin.c | 2 +- src/backend/nodes/queryjumblefuncs.c | 6 +-- src/backend/storage/buffer/bufmgr.c | 14 +++---- src/backend/utils/adt/json.c | 2 +- src/backend/utils/cache/catcache.c | 2 +- src/include/c.h | 9 ++++- src/include/executor/execScan.h | 18 ++++----- 13 files changed, 78 insertions(+), 71 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 6203e3d7f8d..aff02cb003e 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -500,7 +500,7 @@ heap_setscanlimits(TableScanDesc sscan, BlockNumber startBlk, BlockNumber numBlk * multiple times, with constant arguments for all_visible, * check_serializable. */ -pg_attribute_always_inline +pg_always_inline static int page_collect_tuples(HeapScanDesc scan, Snapshot snapshot, Page page, Buffer buffer, diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index e07cb910351..2fd06e37999 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -1105,9 +1105,9 @@ XLogInsertRecord(XLogRecData *rdata, * * NB: Testing shows that XLogInsertRecord runs faster if this code is inlined; * however, because there are two call sites, the compiler is reluctant to - * inline. We use pg_attribute_always_inline here to try to convince it. + * inline. We use pg_always_inline here to try to convince it. */ -static pg_attribute_always_inline void +static pg_always_inline void ReserveXLogInsertLocation(int size, XLogRecPtr *StartPos, XLogRecPtr *EndPos, XLogRecPtr *PrevPtr) { diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c index f5fc346e201..fedaf78ba5e 100644 --- a/src/backend/commands/copyfromparse.c +++ b/src/backend/commands/copyfromparse.c @@ -147,15 +147,15 @@ static int CopyReadAttributesCSV(CopyFromState cstate); static Datum CopyReadBinaryAttribute(CopyFromState cstate, FmgrInfo *flinfo, Oid typioparam, int32 typmod, bool *isnull); -static pg_attribute_always_inline bool CopyFromTextLikeOneRow(CopyFromState cstate, - ExprContext *econtext, - Datum *values, - bool *nulls, - bool is_csv); -static pg_attribute_always_inline bool NextCopyFromRawFieldsInternal(CopyFromState cstate, - char ***fields, - int *nfields, - bool is_csv); +static pg_always_inline bool CopyFromTextLikeOneRow(CopyFromState cstate, + ExprContext *econtext, + Datum *values, + bool *nulls, + bool is_csv); +static pg_always_inline bool NextCopyFromRawFieldsInternal(CopyFromState cstate, + char ***fields, + int *nfields, + bool is_csv); /* Low-level communications functions */ @@ -763,11 +763,11 @@ NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields) * * NOTE: force_not_null option are not applied to the returned fields. * - * We use pg_attribute_always_inline to reduce function call overhead + * We use pg_always_inline to reduce function call overhead * and to help compilers to optimize away the 'is_csv' condition when called * by internal functions such as CopyFromTextLikeOneRow(). */ -static pg_attribute_always_inline bool +static pg_always_inline bool NextCopyFromRawFieldsInternal(CopyFromState cstate, char ***fields, int *nfields, bool is_csv) { int fldct; @@ -930,10 +930,10 @@ CopyFromCSVOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, /* * Workhorse for CopyFromTextOneRow() and CopyFromCSVOneRow(). * - * We use pg_attribute_always_inline to reduce function call overhead + * We use pg_always_inline to reduce function call overhead * and to help compilers to optimize away the 'is_csv' condition. */ -static pg_attribute_always_inline bool +static pg_always_inline bool CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, bool *nulls, bool is_csv) { diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index ea6f18f2c80..307366e5ead 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -257,10 +257,10 @@ CopyToCSVOneRow(CopyToState cstate, TupleTableSlot *slot) /* * Workhorse for CopyToTextOneRow() and CopyToCSVOneRow(). * - * We use pg_attribute_always_inline to reduce function call overhead + * We use pg_always_inline to reduce function call overhead * and to help compilers to optimize away the 'is_csv' condition. */ -static pg_attribute_always_inline void +static pg_always_inline void CopyToTextLikeOneRow(CopyToState cstate, TupleTableSlot *slot, bool is_csv) diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index e5d8345de54..f6093f36afa 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -176,24 +176,24 @@ static Datum ExecJustHashInnerVarVirt(ExprState *state, ExprContext *econtext, b static Datum ExecJustHashOuterVarStrict(ExprState *state, ExprContext *econtext, bool *isnull); /* execution helper functions */ -static pg_attribute_always_inline void ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, - ArrayType *arr, - int16 typlen, - bool typbyval, - char typalign, - bool useOr, - Datum *result, - bool *resultnull); -static pg_attribute_always_inline void ExecAggPlainTransByVal(AggState *aggstate, - AggStatePerTrans pertrans, - AggStatePerGroup pergroup, - ExprContext *aggcontext, - int setno); -static pg_attribute_always_inline void ExecAggPlainTransByRef(AggState *aggstate, - AggStatePerTrans pertrans, - AggStatePerGroup pergroup, - ExprContext *aggcontext, - int setno); +static pg_always_inline void ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, + ArrayType *arr, + int16 typlen, + bool typbyval, + char typalign, + bool useOr, + Datum *result, + bool *resultnull); +static pg_always_inline void ExecAggPlainTransByVal(AggState *aggstate, + AggStatePerTrans pertrans, + AggStatePerGroup pergroup, + ExprContext *aggcontext, + int setno); +static pg_always_inline void ExecAggPlainTransByRef(AggState *aggstate, + AggStatePerTrans pertrans, + AggStatePerGroup pergroup, + ExprContext *aggcontext, + int setno); static char *ExecGetJsonValueItemString(JsonbValue *item, bool *resnull); /* @@ -2550,7 +2550,7 @@ get_cached_rowtype(Oid type_id, int32 typmod, */ /* implementation of ExecJust(Inner|Outer|Scan)Var */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustVarImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) { ExprEvalStep *op = &state->steps[1]; @@ -2588,7 +2588,7 @@ ExecJustScanVar(ExprState *state, ExprContext *econtext, bool *isnull) } /* implementation of ExecJustAssign(Inner|Outer|Scan)Var */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustAssignVarImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull) { ExprEvalStep *op = &state->steps[1]; @@ -2683,7 +2683,7 @@ ExecJustConst(ExprState *state, ExprContext *econtext, bool *isnull) } /* implementation of ExecJust(Inner|Outer|Scan)VarVirt */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustVarVirtImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) { ExprEvalStep *op = &state->steps[0]; @@ -2726,7 +2726,7 @@ ExecJustScanVarVirt(ExprState *state, ExprContext *econtext, bool *isnull) } /* implementation of ExecJustAssign(Inner|Outer|Scan)VarVirt */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustAssignVarVirtImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull) { ExprEvalStep *op = &state->steps[0]; @@ -2805,7 +2805,7 @@ ExecJustHashInnerVarWithIV(ExprState *state, ExprContext *econtext, } /* implementation of ExecJustHash(Inner|Outer)Var */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustHashVarImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) { ExprEvalStep *fetchop = &state->steps[0]; @@ -2843,7 +2843,7 @@ ExecJustHashInnerVar(ExprState *state, ExprContext *econtext, bool *isnull) } /* implementation of ExecJustHash(Inner|Outer)VarVirt */ -static pg_attribute_always_inline Datum +static pg_always_inline Datum ExecJustHashVarVirtImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) { ExprEvalStep *var = &state->steps[0]; @@ -4105,7 +4105,7 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) * Callers must handle the strict LHS-is-NULL; return NULL fast path prior to * calling this. */ -static pg_attribute_always_inline void +static pg_always_inline void ExecEvalArrayCompareInternal(FunctionCallInfo fcinfo, ArrayType *arr, int16 typlen, bool typbyval, char typalign, bool useOr, Datum *result, bool *resultnull) @@ -5903,7 +5903,7 @@ ExecEvalAggOrderedTransTuple(ExprState *state, ExprEvalStep *op, } /* implementation of transition function invocation for byval types */ -static pg_attribute_always_inline void +static pg_always_inline void ExecAggPlainTransByVal(AggState *aggstate, AggStatePerTrans pertrans, AggStatePerGroup pergroup, ExprContext *aggcontext, int setno) @@ -5935,7 +5935,7 @@ ExecAggPlainTransByVal(AggState *aggstate, AggStatePerTrans pertrans, } /* implementation of transition function invocation for byref types */ -static pg_attribute_always_inline void +static pg_always_inline void ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans, AggStatePerGroup pergroup, ExprContext *aggcontext, int setno) diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c index 8e02d68824f..ebe32b411a5 100644 --- a/src/backend/executor/execTuples.c +++ b/src/backend/executor/execTuples.c @@ -72,8 +72,8 @@ static TupleDesc ExecTypeFromTLInternal(List *targetList, bool skipjunk); -static pg_attribute_always_inline void slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, - int natts); +static pg_always_inline void slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, + int natts); static inline void tts_buffer_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple, Buffer buffer, @@ -1118,7 +1118,7 @@ slot_deform_heap_tuple_internal(TupleTableSlot *slot, HeapTuple tuple, * This is marked as always inline, so the different offp for different types * of slots gets optimized away. */ -static pg_attribute_always_inline void +static pg_always_inline void slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, int natts) { diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c index 5661ad76830..039d43ea541 100644 --- a/src/backend/executor/nodeHashjoin.c +++ b/src/backend/executor/nodeHashjoin.c @@ -217,7 +217,7 @@ static void ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate); * the other one is "outer". * ---------------------------------------------------------------- */ -static pg_attribute_always_inline TupleTableSlot * +static pg_always_inline TupleTableSlot * ExecHashJoinImpl(PlanState *pstate, bool parallel) { HashJoinState *node = castNode(HashJoinState, pstate); diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c index 31f97151977..3ce073beb53 100644 --- a/src/backend/nodes/queryjumblefuncs.c +++ b/src/backend/nodes/queryjumblefuncs.c @@ -230,7 +230,7 @@ DoJumble(JumbleState *jstate, Node *node) * * Note: Callers must ensure that size > 0. */ -static pg_attribute_always_inline void +static pg_always_inline void AppendJumbleInternal(JumbleState *jstate, const unsigned char *item, Size size) { @@ -306,7 +306,7 @@ AppendJumble(JumbleState *jstate, const unsigned char *value, Size size) * AppendJumbleNull * For jumbling NULL pointers */ -static pg_attribute_always_inline void +static pg_always_inline void AppendJumbleNull(JumbleState *jstate) { jstate->pending_nulls++; @@ -373,7 +373,7 @@ AppendJumble64(JumbleState *jstate, const unsigned char *value) * * Note: Callers must ensure that there's at least 1 pending NULL. */ -static pg_attribute_always_inline void +static pg_always_inline void FlushPendingNulls(JumbleState *jstate) { Assert(jstate->pending_nulls > 0); diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index a5b4bc5b7d5..9a771c81cb1 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -1097,7 +1097,7 @@ ZeroAndLockBuffer(Buffer buffer, ReadBufferMode mode, bool already_valid) * already present, or false if more work is required to either read it in or * zero it. */ -static pg_attribute_always_inline Buffer +static pg_always_inline Buffer PinBufferForBlock(Relation rel, SMgrRelation smgr, char persistence, @@ -1180,7 +1180,7 @@ PinBufferForBlock(Relation rel, * * smgr is required, rel is optional unless using P_NEW. */ -static pg_attribute_always_inline Buffer +static pg_always_inline Buffer ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence, ForkNumber forkNum, BlockNumber blockNum, ReadBufferMode mode, @@ -1261,7 +1261,7 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence, return buffer; } -static pg_attribute_always_inline bool +static pg_always_inline bool StartReadBuffersImpl(ReadBuffersOperation *operation, Buffer *buffers, BlockNumber blockNum, @@ -2005,7 +2005,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress) * * No locks are held either at entry or exit. */ -static pg_attribute_always_inline BufferDesc * +static pg_always_inline BufferDesc * BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, BlockNumber blockNum, BufferAccessStrategy strategy, @@ -6803,7 +6803,7 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, * part of error handling, which in turn could lead to the buffer being * replaced while IO is ongoing. */ -static pg_attribute_always_inline void +static pg_always_inline void buffer_stage_common(PgAioHandle *ioh, bool is_write, bool is_temp) { uint64 *io_data; @@ -7049,7 +7049,7 @@ buffer_readv_encode_error(PgAioResult *result, * Helper for AIO readv completion callbacks, supporting both shared and temp * buffers. Gets called once for each buffer in a multi-page read. */ -static pg_attribute_always_inline void +static pg_always_inline void buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer, uint8 flags, bool failed, bool is_temp, bool *buffer_invalid, @@ -7193,7 +7193,7 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer, * * Shared between shared and local buffers, to reduce code duplication. */ -static pg_attribute_always_inline PgAioResult +static pg_always_inline PgAioResult buffer_readv_complete(PgAioHandle *ioh, PgAioResult prior_result, uint8 cb_data, bool is_temp) { diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 51452755f58..c1d3de602ae 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -1558,7 +1558,7 @@ json_object_two_arg(PG_FUNCTION_ARGS) * escape_json_char * Inline helper function for escape_json* functions */ -static pg_attribute_always_inline void +static pg_always_inline void escape_json_char(StringInfo buf, char c) { switch (c) diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index c1bca66a092..a2253e5209e 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -1070,7 +1070,7 @@ RehashCatCacheLists(CatCache *cp) * * Call CatalogCacheInitializeCache() if not yet done. */ -pg_attribute_always_inline +pg_always_inline static void ConditionalCatalogCacheInitializeCache(CatCache *cache) { diff --git a/src/include/c.h b/src/include/c.h index 508c007cedd..ae9dbaf22f3 100644 --- a/src/include/c.h +++ b/src/include/c.h @@ -264,19 +264,26 @@ #endif /* - * Use "pg_attribute_always_inline" in place of "inline" for functions that + * Use "pg_always_inline" in place of "inline" for functions that * we wish to force inlining of, even when the compiler's heuristics would * choose not to. But, if possible, don't force inlining in unoptimized * debug builds. + * + * XXX The "pg_attribute_always_inline" variant is kept for backwards + * compatibility with existing code. All new code should use the shorter + * variant "pg_always_inline." */ #if (defined(__GNUC__) && __GNUC__ > 3 && defined(__OPTIMIZE__)) || defined(__SUNPRO_C) /* GCC > 3 and Sunpro support always_inline via __attribute__ */ +#define pg_always_inline __attribute__((always_inline)) inline #define pg_attribute_always_inline __attribute__((always_inline)) inline #elif defined(_MSC_VER) /* MSVC has a special keyword for this */ +#define pg_always_inline __forceinline #define pg_attribute_always_inline __forceinline #else /* Otherwise, the best we can do is to say "inline" */ +#define pg_always_inline inline #define pg_attribute_always_inline inline #endif diff --git a/src/include/executor/execScan.h b/src/include/executor/execScan.h index 2003cbc7ed5..2a66639eed4 100644 --- a/src/include/executor/execScan.h +++ b/src/include/executor/execScan.h @@ -23,12 +23,12 @@ * This routine substitutes a test tuple if inside an EvalPlanQual recheck. * Otherwise, it simply executes the access method's next-tuple routine. * - * The pg_attribute_always_inline attribute allows the compiler to inline - * this function into its caller. When EPQState is NULL, the EvalPlanQual - * logic is completely eliminated at compile time, avoiding unnecessary - * run-time checks and code for cases where EPQ is not required. + * The pg_always_inline attribute allows the compiler to inline this function + * into its caller. When EPQState is NULL, the EvalPlanQual logic is completely + * eliminated at compile time, avoiding unnecessary run-time checks and code + * for cases where EPQ is not required. */ -static pg_attribute_always_inline TupleTableSlot * +static pg_always_inline TupleTableSlot * ExecScanFetch(ScanState *node, EPQState *epqstate, ExecScanAccessMtd accessMtd, @@ -144,9 +144,9 @@ ExecScanFetch(ScanState *node, * conditions enforced by the access method. * * This function is an alternative to ExecScan, used when callers may omit - * 'qual' or 'projInfo'. The pg_attribute_always_inline attribute allows the - * compiler to eliminate non-relevant branches at compile time, avoiding - * run-time checks in those cases. + * 'qual' or 'projInfo'. The pg_always_inline attribute allows the compiler + * to eliminate non-relevant branches at compile time, avoiding run-time + * checks in those cases. * * Conditions: * -- The AMI "cursor" is positioned at the previously returned tuple. @@ -156,7 +156,7 @@ ExecScanFetch(ScanState *node, * positioned before the first qualifying tuple. * ---------------------------------------------------------------- */ -static pg_attribute_always_inline TupleTableSlot * +static pg_always_inline TupleTableSlot * ExecScanExtended(ScanState *node, ExecScanAccessMtd accessMtd, /* function returning a tuple */ ExecScanRecheckMtd recheckMtd, From 18105e6db5e5314a575fdb23a99cf4809d8ef062 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Wed, 15 Jul 2026 09:22:58 +0900 Subject: [PATCH 139/250] Strip removed-relation references from PHVs in join clauses Commit 9a60f295b stripped the stale PlaceHolderVars left behind by left-join removal from the surviving rels' baserestrictinfo and from EquivalenceClass member expressions, but it overlooked join clauses. A PlaceHolderVar embedded in a join clause can likewise retain the removed rel and join in its phrels, since remove_rel_from_query() fixes up the RestrictInfo's own relid sets but not the PHVs inside its expression. As before, this is normally harmless, because later processing consults those relid sets rather than the embedded PHVs. However, a restriction clause derived from such an OR join clause inherits the stale PlaceHolderVar, and when the derived clause is translated for an appendrel child, pull_varnos() recomputes its relids and folds the removed relation back in. The rebuilt clause then references a no-longer-existent relation, tripping an assertion during path generation. Fix by also stripping the removed relation from the PlaceHolderVars in the surviving rels' join clauses, including the sub-clauses of any OR clause. Like 9a60f295b, this is only reachable on v18 and later, where match_index_to_operand() began ignoring PlaceHolderVars. Author: Arne Roland Reviewed-by: Tender Wang Reviewed-by: Richard Guo Discussion: https://postgr.es/m/27a44087-3d65-473e-8d88-7c12228e0d7e@malkut.net Backpatch-through: 18 --- src/backend/optimizer/plan/analyzejoins.c | 79 +++++++++++++++++++++-- src/test/regress/expected/join.out | 11 ++++ src/test/regress/sql/join.sql | 7 ++ 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 5557600988d..4d19204da0a 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -75,6 +75,8 @@ static void remove_rel_from_restrictinfo(RestrictInfo *rinfo, static void remove_rel_from_eclass(PlannerInfo *root, EquivalenceClass *ec, SpecialJoinInfo *sjinfo, int relid, int subst); +static void remove_rel_from_restrictinfo_phvs(RestrictInfo *rinfo, + int relid, int ojrelid); static Node *remove_rel_from_phvs(Node *node, int relid, int ojrelid); static Node *remove_rel_from_phvs_mutator(Node *node, Relids removable); static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved); @@ -347,6 +349,7 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, int relid = rel->relid; Index rti; ListCell *l; + Bitmapset *seen_serials = NULL; /* * Update all_baserels and related relid sets. @@ -522,9 +525,9 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, * lateral_vars lists. (We already did this above for ph_needed.) * * Also, for left-join removal, we strip the removed rel and join from any - * PlaceHolderVar embedded in the surviving rels' restriction clauses (see - * remove_rel_from_phvs); we needn't bother with the rel being removed, - * nor when the query has no PlaceHolderVars. + * PlaceHolderVar embedded in the surviving rels' restriction clauses and + * join clauses; we needn't bother with the rel being removed, nor when + * the query has no PlaceHolderVars. */ for (rti = 1; rti < root->simple_rel_array_size; rti++) { @@ -554,10 +557,27 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, if (sjinfo != NULL && rti != relid && root->glob->lastPHId != 0) { foreach_node(RestrictInfo, rinfo, otherrel->baserestrictinfo) + remove_rel_from_restrictinfo_phvs(rinfo, relid, sjinfo->ojrelid); + + /* + * Join clauses need the same treatment, but there's no value in + * processing any join clause more than once. So it's slightly + * annoying that we have to find them via the per-base-relation + * joininfo lists. Avoid duplicate processing by tracking the + * rinfo_serial numbers of join clauses we've already seen. (This + * doesn't work for is_clone clauses, so we must waste effort on + * them.) + */ + foreach_node(RestrictInfo, rinfo, otherrel->joininfo) { - rinfo->clause = (Expr *) - remove_rel_from_phvs((Node *) rinfo->clause, relid, - sjinfo->ojrelid); + if (!rinfo->is_clone) /* else serial number is not unique */ + { + if (bms_is_member(rinfo->rinfo_serial, seen_serials)) + continue; /* saw it already */ + seen_serials = bms_add_member(seen_serials, + rinfo->rinfo_serial); + } + remove_rel_from_restrictinfo_phvs(rinfo, relid, sjinfo->ojrelid); } } } @@ -847,6 +867,53 @@ remove_rel_from_eclass(PlannerInfo *root, EquivalenceClass *ec, ec_clear_derived_clauses(ec); } +/* + * Remove any references to relid or ojrelid from the PlaceHolderVars embedded + * in a RestrictInfo's clause. + * + * If it's an OR clause, we must also fix up the orclause, which is a parallel + * representation built from its own sub-RestrictInfos. We recurse into the + * sub-clauses for that, mirroring remove_rel_from_restrictinfo. + */ +static void +remove_rel_from_restrictinfo_phvs(RestrictInfo *rinfo, int relid, int ojrelid) +{ + rinfo->clause = (Expr *) + remove_rel_from_phvs((Node *) rinfo->clause, relid, ojrelid); + + /* If it's an OR, recurse to clean up sub-clauses */ + if (restriction_is_or_clause(rinfo)) + { + ListCell *lc; + + Assert(is_orclause(rinfo->orclause)); + foreach(lc, ((BoolExpr *) rinfo->orclause)->args) + { + Node *orarg = (Node *) lfirst(lc); + + /* OR arguments should be ANDs or sub-RestrictInfos */ + if (is_andclause(orarg)) + { + List *andargs = ((BoolExpr *) orarg)->args; + ListCell *lc2; + + foreach(lc2, andargs) + { + RestrictInfo *rinfo2 = lfirst_node(RestrictInfo, lc2); + + remove_rel_from_restrictinfo_phvs(rinfo2, relid, ojrelid); + } + } + else + { + RestrictInfo *rinfo2 = castNode(RestrictInfo, orarg); + + remove_rel_from_restrictinfo_phvs(rinfo2, relid, ojrelid); + } + } + } +} + /* * Remove any references to the specified RT index(es) from the phrels (and * phnullingrels) of every PlaceHolderVar in the given expression. diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index e9a5ecb1581..86078cbf27d 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6394,6 +6394,17 @@ group by (); Result (1 row) +-- likewise for a PHV embedded in an OR join clause +explain (costs off) +select 1 from parted_b t1 + join (select t2.id from parted_b t2 left join parted_b t3 on t2.id = t3.id) s + on (t1.id = 1 and s.id = 2) or (t1.id = 3 and s.id = 4) +group by (); + QUERY PLAN +------------ + Result +(1 row) + rollback; create temp table parent (k int primary key, pd int); create temp table child (k int unique, cd int); diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index d0f36bec25c..3aaa882d668 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -2354,6 +2354,13 @@ select 1 from parted_b t1 on t1.id = s.id group by (); +-- likewise for a PHV embedded in an OR join clause +explain (costs off) +select 1 from parted_b t1 + join (select t2.id from parted_b t2 left join parted_b t3 on t2.id = t3.id) s + on (t1.id = 1 and s.id = 2) or (t1.id = 3 and s.id = 4) +group by (); + rollback; create temp table parent (k int primary key, pd int); From 36c6b499761878d40fd5f62b698546374a8aa5bc Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Wed, 15 Jul 2026 10:03:39 +0900 Subject: [PATCH 140/250] Include check on polpermissive relcache for policies equalPolicy() is used in the relation cache to check if two policy definitions are equivalent, but missed to check for polpermissive. ALTER POLICY cannot switch a policy to be PERMISSIVE or RESTRICTIVE, so this would need a dropped and then re-created policy, which would trigger a relcache invalidation. Anyway, there is no harm in being consistent in the check, and if one decides to add an ALTER POLICY to switch PERMISSIVE or RESTRICTIVE, we would be silently in trouble. Author: Andreas Lind Reviewed-by: Laurenz Albe Discussion: https://postgr.es/m/CAMxA3rv1CS6R7JR5ojz-3CmCEnZEFrqu+XXTnGbLRWrjJRH7sA@mail.gmail.com Backpatch-through: 14 --- src/backend/utils/cache/relcache.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 559ba9cdb2c..2186a6ffeba 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -982,6 +982,8 @@ equalPolicy(RowSecurityPolicy *policy1, RowSecurityPolicy *policy2) if (policy1->polcmd != policy2->polcmd) return false; + if (policy1->permissive != policy2->permissive) + return false; if (policy1->hassublinks != policy2->hassublinks) return false; if (strcmp(policy1->policy_name, policy2->policy_name) != 0) From ae8c4bd558c5d55c5a357e910d496eea8ff33647 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 15 Jul 2026 21:22:38 +0900 Subject: [PATCH 141/250] Fix argument names in pg_clear_attribute_stats() errors pg_clear_attribute_stats() checks its required arguments manually because the function is not strict. Previously, when schemaname or relname was passed as NULL, the error incorrectly reported the argument name as "relation" in both cases: ERROR: argument "relation" must not be null This was misleading, especially for schemaname, and inconsistent with the function's SQL-visible argument names. The cause is that cleararginfo[] in attribute_stats.c used "relation" for both the schema-name and relation-name arguments. This commit fixes the issue by using "schemaname" and "relname" instead, matching the function's declared argument names so that the error reports the correct argument name. Backpatch to v18, where pg_clear_attribute_stats() was introduced. Author: Ilia Evdokimov Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/4bf66c5e-8dd7-4ef3-8691-db67ecff6f16@tantorlabs.com Backpatch-through: 18 --- src/backend/statistics/attribute_stats.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index 5c1c5749ad4..357466456ff 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -93,8 +93,8 @@ enum clear_attribute_stats_argnum static struct StatsArgInfo cleararginfo[] = { - [C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID}, - [C_ATTRELNAME_ARG] = {"relation", TEXTOID}, + [C_ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID}, + [C_ATTRELNAME_ARG] = {"relname", TEXTOID}, [C_ATTNAME_ARG] = {"attname", TEXTOID}, [C_INHERITED_ARG] = {"inherited", BOOLOID}, [C_NUM_ATTRIBUTE_STATS_ARGS] = {0} From bcc428a23a69ee6f3bcc2ce20655da68eed0e7aa Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Wed, 15 Jul 2026 13:04:32 -0400 Subject: [PATCH 142/250] Add additional sanity checks when reading a blkreftable. Code elsewhere in the system assumes that fork numbers and chunk sizes are within bounds, so the code that reads those quantities from disk should validate that they are. Without these additional checks, a corrupted file can cause us to index off the end of fork number or chunk entry arrays, potentially resulting in a crash. Reported-by: oxsignal (chunk sizes) Reported-by: Robert Haas (fork numbers) Reviewed-by: Daniel Gustafsson Discussion: http://postgr.es/m/CA+TgmoYP8RKoBGosS7C6Fdr-GNCfyz_W1zmK=Tx1Fe0ZvzGh0g@mail.gmail.com Backpatch-through: 17 --- src/common/blkreftable.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/common/blkreftable.c b/src/common/blkreftable.c index 5c85d6a5c6d..b33687a9f36 100644 --- a/src/common/blkreftable.c +++ b/src/common/blkreftable.c @@ -657,6 +657,15 @@ BlockRefTableReaderNextRelation(BlockRefTableReader *reader, return false; } + /* Sanity-check the fork number. */ + if (sentry.forknum < 0 || sentry.forknum > MAX_FORKNUM) + { + reader->error_callback(reader->error_callback_arg, + "file \"%s\" has invalid fork number %d", + reader->error_filename, sentry.forknum); + return false; + } + /* * Sanity-check the nchunks value. In the backend, palloc_array would * enforce this anyway (with a more generic error message); but in @@ -678,6 +687,19 @@ BlockRefTableReaderNextRelation(BlockRefTableReader *reader, BlockRefTableRead(reader, reader->chunk_size, sentry.nchunks * sizeof(uint16)); + /* Sanity-check the chunk sizes. */ + for (unsigned i = 0; i < sentry.nchunks; ++i) + { + if (reader->chunk_size[i] > MAX_ENTRIES_PER_CHUNK) + { + reader->error_callback(reader->error_callback_arg, + "file \"%s\" chunk %u has invalid size %u", + reader->error_filename, i, + (unsigned) reader->chunk_size[i]); + return false; + } + } + /* Set up for chunk scan. */ reader->total_chunks = sentry.nchunks; reader->consumed_chunks = 0; From a1b962b366210ab8d8d220582ad1c32620f35935 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Wed, 15 Jul 2026 21:40:09 +0200 Subject: [PATCH 143/250] pgbench: Fix incorrect parameter name in error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 6f164e6d17616 accidentally mistyped --client as --clients in the error message. Backpatch down to v15 where the it was introduced. Author: Semih Doğan Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/CALOtZ7tuWisV=v0cUY_q6PLHJ-fOiQ7ZN476JwmM0PyV0t5i7Q@mail.gmail.com Backpatch-through: 15 --- src/bin/pgbench/pgbench.c | 2 +- src/bin/pgbench/t/002_pgbench_no_server.pl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c index 7913dde6ceb..6d3b67cdede 100644 --- a/src/bin/pgbench/pgbench.c +++ b/src/bin/pgbench/pgbench.c @@ -6845,7 +6845,7 @@ main(int argc, char **argv) break; case 'c': benchmarking_option_set = true; - if (!option_parse_int(optarg, "-c/--clients", 1, INT_MAX, + if (!option_parse_int(optarg, "-c/--client", 1, INT_MAX, &nclients)) { exit(1); diff --git a/src/bin/pgbench/t/002_pgbench_no_server.pl b/src/bin/pgbench/t/002_pgbench_no_server.pl index f975c73dd75..ded4356f95c 100644 --- a/src/bin/pgbench/t/002_pgbench_no_server.pl +++ b/src/bin/pgbench/t/002_pgbench_no_server.pl @@ -92,7 +92,7 @@ sub pgbench_scripts [ 'too many scripts', '-S ' x 129, [qr{at most 128 SQL scripts}] ], [ 'bad #clients', '-c three', - [qr{invalid value "three" for option -c/--clients}] + [qr{invalid value "three" for option -c/--client}] ], [ 'bad #threads', '-j eleven', From eabc9a9dd908a1be7d66dc26b378b1488fd14847 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Wed, 15 Jul 2026 15:49:59 -0400 Subject: [PATCH 144/250] Include last block in FSM vacuum of bulk extended relation When bulk-extending a relation, we add the newly-added blocks that we won't immediately use to the free space map and then call FreeSpaceMapVacuumRange() to propagate that free space up the FSM tree, so other backends can find and reuse it. However, the end block argument to FreeSpaceMapVacuumRange() is exclusive, and we passed the number of the last added block (since 00d1e02be24). If that block was the first one covered by a new FSM page, its free space wasn't propagated up the tree and was therefore invisible to FSM searches until the next FSM vacuum. Fix by passing the block number one past the last added block, so the full range is vacuumed. Author: Jingtang Zhang Reviewed-by: Melanie Plageman Discussion: https://postgr.es/m/flat/CAPsk3_Bx_vdybN%3D-DZu8HLStf%2BXnuFUBkLwxouONSMkWuO9oug%40mail.gmail.com Backpatch-through: 16 --- src/backend/access/heap/hio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c index c482c9d61b2..242564d0c75 100644 --- a/src/backend/access/heap/hio.c +++ b/src/backend/access/heap/hio.c @@ -403,7 +403,7 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate, { BlockNumber first_fsm_block = first_block + not_in_fsm_pages; - FreeSpaceMapVacuumRange(relation, first_fsm_block, last_block); + FreeSpaceMapVacuumRange(relation, first_fsm_block, last_block + 1); } if (bistate) From d0fb1da21bbcc7bdf13632cd080628757cd008e3 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Wed, 15 Jul 2026 17:37:34 -0400 Subject: [PATCH 145/250] Introduce macros for WAL block reference IDs of some heap record types When registering a buffer with the WAL machinery, the caller assigns it a block reference ID, and replay must read each block back by that same ID. Today these IDs are bare integers assigned by convention (0, 1, 2, ...), which is easy to follow when a record registers a single block, or when the blocks are handled during replay in their registration order. An upcoming bug fix registers up to two visibility map blocks in addition to the heap block(s) when clearing the VM, and these are not handled during replay in a straightforward 1:1, in-registration-order fashion. Relying on bare integers for the block IDs in that case is error-prone. Introduce macros naming the block reference IDs for the heap record types that the upcoming commit extends to register visibility map blocks, so the registration and replay sites refer to the same block by a meaningful name. Author: Melanie Plageman Reviewed-by: Robert Haas Discussion: https://postgr.es/m/66mqpfyti3qhfttcsv6r2lbvqqd32rrmpn6i47ovrsnvguts46%40gou54xc> Backpatch through: 17 --- src/backend/access/heap/heapam.c | 43 +++++++++++--------- src/backend/access/heap/heapam_xlog.c | 56 +++++++++++++++++---------- src/include/access/heapam_xlog.h | 28 ++++++++++---- 3 files changed, 82 insertions(+), 45 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index aff02cb003e..cf37956a2bf 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -2218,10 +2218,12 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, * write the whole page to the xlog, we don't need to store * xl_heap_header in the xlog. */ - XLogRegisterBuffer(0, buffer, REGBUF_STANDARD | bufflags); - XLogRegisterBufData(0, &xlhdr, SizeOfHeapHeader); + XLogRegisterBuffer(HEAP_INSERT_BLKREF_HEAP, buffer, + REGBUF_STANDARD | bufflags); + XLogRegisterBufData(HEAP_INSERT_BLKREF_HEAP, &xlhdr, + SizeOfHeapHeader); /* PG73FORMAT: write bitmap [+ padding] [+ oid] + data */ - XLogRegisterBufData(0, + XLogRegisterBufData(HEAP_INSERT_BLKREF_HEAP, (char *) heaptup->t_data + SizeofHeapTupleHeader, heaptup->t_len - SizeofHeapTupleHeader); @@ -2619,9 +2621,11 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, XLogBeginInsert(); XLogRegisterData(xlrec, tupledata - scratch.data); - XLogRegisterBuffer(0, buffer, REGBUF_STANDARD | bufflags); + XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_HEAP, buffer, + REGBUF_STANDARD | bufflags); - XLogRegisterBufData(0, tupledata, totaldatalen); + XLogRegisterBufData(HEAP_MULTI_INSERT_BLKREF_HEAP, tupledata, + totaldatalen); /* filtering by origin on a row level is much more efficient */ XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN); @@ -3112,7 +3116,7 @@ heap_delete(Relation relation, ItemPointer tid, XLogBeginInsert(); XLogRegisterData(&xlrec, SizeOfHeapDelete); - XLogRegisterBuffer(0, buffer, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_DELETE_BLKREF_HEAP, buffer, REGBUF_STANDARD); /* * Log replica identity of the deleted tuple if there is one @@ -3883,7 +3887,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, XLogRecPtr recptr; XLogBeginInsert(); - XLogRegisterBuffer(0, buffer, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_LOCK_BLKREF_HEAP, buffer, REGBUF_STANDARD); xlrec.offnum = ItemPointerGetOffsetNumber(&oldtup.t_self); xlrec.xmax = xmax_lock_old_tuple; @@ -5219,7 +5223,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, XLogRecPtr recptr; XLogBeginInsert(); - XLogRegisterBuffer(0, *buffer, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_LOCK_BLKREF_HEAP, *buffer, REGBUF_STANDARD); xlrec.offnum = ItemPointerGetOffsetNumber(&tuple->t_self); xlrec.xmax = xid; @@ -5971,7 +5975,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, Page page = BufferGetPage(buf); XLogBeginInsert(); - XLogRegisterBuffer(0, buf, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_LOCK_BLKREF_HEAP, buf, REGBUF_STANDARD); xlrec.offnum = ItemPointerGetOffsetNumber(&mytup.t_self); xlrec.xmax = new_xmax; @@ -8972,9 +8976,9 @@ log_heap_update(Relation reln, Buffer oldbuf, if (need_tuple_data) bufflags |= REGBUF_KEEP_DATA; - XLogRegisterBuffer(0, newbuf, bufflags); + XLogRegisterBuffer(HEAP_UPDATE_BLKREF_HEAP_NEW, newbuf, bufflags); if (oldbuf != newbuf) - XLogRegisterBuffer(1, oldbuf, REGBUF_STANDARD); + XLogRegisterBuffer(HEAP_UPDATE_BLKREF_HEAP_OLD, oldbuf, REGBUF_STANDARD); XLogRegisterData(&xlrec, SizeOfHeapUpdate); @@ -8987,15 +8991,18 @@ log_heap_update(Relation reln, Buffer oldbuf, { prefix_suffix[0] = prefixlen; prefix_suffix[1] = suffixlen; - XLogRegisterBufData(0, &prefix_suffix, sizeof(uint16) * 2); + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, &prefix_suffix, + sizeof(uint16) * 2); } else if (prefixlen > 0) { - XLogRegisterBufData(0, &prefixlen, sizeof(uint16)); + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, &prefixlen, + sizeof(uint16)); } else { - XLogRegisterBufData(0, &suffixlen, sizeof(uint16)); + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, &suffixlen, + sizeof(uint16)); } } @@ -9009,10 +9016,10 @@ log_heap_update(Relation reln, Buffer oldbuf, * * The 'data' doesn't include the common prefix or suffix. */ - XLogRegisterBufData(0, &xlhdr, SizeOfHeapHeader); + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, &xlhdr, SizeOfHeapHeader); if (prefixlen == 0) { - XLogRegisterBufData(0, + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, (char *) newtup->t_data + SizeofHeapTupleHeader, newtup->t_len - SizeofHeapTupleHeader - suffixlen); } @@ -9025,13 +9032,13 @@ log_heap_update(Relation reln, Buffer oldbuf, /* bitmap [+ padding] [+ oid] */ if (newtup->t_data->t_hoff - SizeofHeapTupleHeader > 0) { - XLogRegisterBufData(0, + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, (char *) newtup->t_data + SizeofHeapTupleHeader, newtup->t_data->t_hoff - SizeofHeapTupleHeader); } /* data after common prefix */ - XLogRegisterBufData(0, + XLogRegisterBufData(HEAP_UPDATE_BLKREF_HEAP_NEW, (char *) newtup->t_data + newtup->t_data->t_hoff + prefixlen, newtup->t_len - newtup->t_data->t_hoff - prefixlen - suffixlen); } diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c index eb4bd3d6ae3..87b5c44fd46 100644 --- a/src/backend/access/heap/heapam_xlog.c +++ b/src/backend/access/heap/heapam_xlog.c @@ -350,7 +350,8 @@ heap_xlog_delete(XLogReaderState *record) RelFileLocator target_locator; ItemPointerData target_tid; - XLogRecGetBlockTag(record, 0, &target_locator, NULL, &blkno); + XLogRecGetBlockTag(record, HEAP_DELETE_BLKREF_HEAP, &target_locator, NULL, + &blkno); ItemPointerSetBlockNumber(&target_tid, blkno); ItemPointerSetOffsetNumber(&target_tid, xlrec->offnum); @@ -369,7 +370,8 @@ heap_xlog_delete(XLogReaderState *record) FreeFakeRelcacheEntry(reln); } - if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) + if (XLogReadBufferForRedo(record, HEAP_DELETE_BLKREF_HEAP, + &buffer) == BLK_NEEDS_REDO) { page = BufferGetPage(buffer); @@ -434,7 +436,8 @@ heap_xlog_insert(XLogReaderState *record) ItemPointerData target_tid; XLogRedoAction action; - XLogRecGetBlockTag(record, 0, &target_locator, NULL, &blkno); + XLogRecGetBlockTag(record, HEAP_INSERT_BLKREF_HEAP, &target_locator, NULL, + &blkno); ItemPointerSetBlockNumber(&target_tid, blkno); ItemPointerSetOffsetNumber(&target_tid, xlrec->offnum); @@ -462,13 +465,14 @@ heap_xlog_insert(XLogReaderState *record) */ if (XLogRecGetInfo(record) & XLOG_HEAP_INIT_PAGE) { - buffer = XLogInitBufferForRedo(record, 0); + buffer = XLogInitBufferForRedo(record, HEAP_INSERT_BLKREF_HEAP); page = BufferGetPage(buffer); PageInit(page, BufferGetPageSize(buffer), 0); action = BLK_NEEDS_REDO; } else - action = XLogReadBufferForRedo(record, 0, &buffer); + action = XLogReadBufferForRedo(record, HEAP_INSERT_BLKREF_HEAP, + &buffer); if (action == BLK_NEEDS_REDO) { Size datalen; @@ -479,7 +483,7 @@ heap_xlog_insert(XLogReaderState *record) if (PageGetMaxOffsetNumber(page) + 1 < xlrec->offnum) elog(PANIC, "invalid max offset number"); - data = XLogRecGetBlockData(record, 0, &datalen); + data = XLogRecGetBlockData(record, HEAP_INSERT_BLKREF_HEAP, &datalen); newlen = datalen - SizeOfHeapHeader; Assert(datalen > SizeOfHeapHeader && newlen <= MaxHeapTupleSize); @@ -559,7 +563,8 @@ heap_xlog_multi_insert(XLogReaderState *record) */ xlrec = (xl_heap_multi_insert *) XLogRecGetData(record); - XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno); + XLogRecGetBlockTag(record, HEAP_MULTI_INSERT_BLKREF_HEAP, &rlocator, NULL, + &blkno); /* check that the mutually exclusive flags are not both set */ Assert(!((xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) && @@ -582,13 +587,14 @@ heap_xlog_multi_insert(XLogReaderState *record) if (isinit) { - buffer = XLogInitBufferForRedo(record, 0); + buffer = XLogInitBufferForRedo(record, HEAP_MULTI_INSERT_BLKREF_HEAP); page = BufferGetPage(buffer); PageInit(page, BufferGetPageSize(buffer), 0); action = BLK_NEEDS_REDO; } else - action = XLogReadBufferForRedo(record, 0, &buffer); + action = XLogReadBufferForRedo(record, HEAP_MULTI_INSERT_BLKREF_HEAP, + &buffer); if (action == BLK_NEEDS_REDO) { char *tupdata; @@ -596,7 +602,8 @@ heap_xlog_multi_insert(XLogReaderState *record) Size len; /* Tuples are stored as block data */ - tupdata = XLogRecGetBlockData(record, 0, &len); + tupdata = XLogRecGetBlockData(record, HEAP_MULTI_INSERT_BLKREF_HEAP, + &len); endptr = tupdata + len; page = (Page) BufferGetPage(buffer); @@ -713,8 +720,10 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) oldtup.t_data = NULL; oldtup.t_len = 0; - XLogRecGetBlockTag(record, 0, &rlocator, NULL, &newblk); - if (XLogRecGetBlockTagExtended(record, 1, NULL, NULL, &oldblk, NULL)) + XLogRecGetBlockTag(record, HEAP_UPDATE_BLKREF_HEAP_NEW, &rlocator, NULL, + &newblk); + if (XLogRecGetBlockTagExtended(record, HEAP_UPDATE_BLKREF_HEAP_OLD, NULL, NULL, + &oldblk, NULL)) { /* HOT updates are never done across pages */ Assert(!hot_update); @@ -750,7 +759,8 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) */ /* Deal with old tuple version */ - oldaction = XLogReadBufferForRedo(record, (oldblk == newblk) ? 0 : 1, + oldaction = XLogReadBufferForRedo(record, (oldblk == newblk) ? + HEAP_UPDATE_BLKREF_HEAP_NEW : HEAP_UPDATE_BLKREF_HEAP_OLD, &obuffer); if (oldaction == BLK_NEEDS_REDO) { @@ -800,13 +810,14 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) } else if (XLogRecGetInfo(record) & XLOG_HEAP_INIT_PAGE) { - nbuffer = XLogInitBufferForRedo(record, 0); + nbuffer = XLogInitBufferForRedo(record, HEAP_UPDATE_BLKREF_HEAP_NEW); page = (Page) BufferGetPage(nbuffer); PageInit(page, BufferGetPageSize(nbuffer), 0); newaction = BLK_NEEDS_REDO; } else - newaction = XLogReadBufferForRedo(record, 0, &nbuffer); + newaction = XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_HEAP_NEW, + &nbuffer); /* * The visibility map may need to be fixed even if the heap page is @@ -831,7 +842,8 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) Size datalen; Size tuplen; - recdata = XLogRecGetBlockData(record, 0, &datalen); + recdata = XLogRecGetBlockData(record, HEAP_UPDATE_BLKREF_HEAP_NEW, + &datalen); recdata_end = recdata + datalen; page = BufferGetPage(nbuffer); @@ -1015,7 +1027,8 @@ heap_xlog_lock(XLogReaderState *record) BlockNumber block; Relation reln; - XLogRecGetBlockTag(record, 0, &rlocator, NULL, &block); + XLogRecGetBlockTag(record, HEAP_LOCK_BLKREF_HEAP, &rlocator, NULL, + &block); reln = CreateFakeRelcacheEntry(rlocator); visibilitymap_pin(reln, block, &vmbuffer); @@ -1025,7 +1038,8 @@ heap_xlog_lock(XLogReaderState *record) FreeFakeRelcacheEntry(reln); } - if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) + if (XLogReadBufferForRedo(record, HEAP_LOCK_BLKREF_HEAP, + &buffer) == BLK_NEEDS_REDO) { page = (Page) BufferGetPage(buffer); @@ -1091,7 +1105,8 @@ heap_xlog_lock_updated(XLogReaderState *record) BlockNumber block; Relation reln; - XLogRecGetBlockTag(record, 0, &rlocator, NULL, &block); + XLogRecGetBlockTag(record, HEAP_LOCK_BLKREF_HEAP, &rlocator, NULL, + &block); reln = CreateFakeRelcacheEntry(rlocator); visibilitymap_pin(reln, block, &vmbuffer); @@ -1101,7 +1116,8 @@ heap_xlog_lock_updated(XLogReaderState *record) FreeFakeRelcacheEntry(reln); } - if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) + if (XLogReadBufferForRedo(record, HEAP_LOCK_BLKREF_HEAP, + &buffer) == BLK_NEEDS_REDO) { page = BufferGetPage(buffer); diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h index 277df6b3cf0..92744c53899 100644 --- a/src/include/access/heapam_xlog.h +++ b/src/include/access/heapam_xlog.h @@ -110,6 +110,8 @@ (XLH_DELETE_CONTAINS_OLD_TUPLE | XLH_DELETE_CONTAINS_OLD_KEY) /* This is what we need to know about delete */ +#define HEAP_DELETE_BLKREF_HEAP 0 + typedef struct xl_heap_delete { TransactionId xmax; /* xmax of the deleted tuple */ @@ -157,12 +159,14 @@ typedef struct xl_heap_header #define SizeOfHeapHeader (offsetof(xl_heap_header, t_hoff) + sizeof(uint8)) /* This is what we need to know about insert */ +#define HEAP_INSERT_BLKREF_HEAP 0 + typedef struct xl_heap_insert { OffsetNumber offnum; /* inserted tuple's offset */ uint8 flags; - /* xl_heap_header & TUPLE DATA in backup block 0 */ + /* xl_heap_header & TUPLE DATA in HEAP_INSERT_BLKREF_HEAP */ } xl_heap_insert; #define SizeOfHeapInsert (offsetof(xl_heap_insert, flags) + sizeof(uint8)) @@ -173,10 +177,13 @@ typedef struct xl_heap_insert * The main data of the record consists of this xl_heap_multi_insert header. * 'offsets' array is omitted if the whole page is reinitialized * (XLOG_HEAP_INIT_PAGE). - * - * In block 0's data portion, there is an xl_multi_insert_tuple struct, - * followed by the tuple data for each tuple. There is padding to align - * each xl_multi_insert_tuple struct. + */ +#define HEAP_MULTI_INSERT_BLKREF_HEAP 0 + +/* + * In HEAP_MULTI_INSERT_BLKREF_HEAP's data portion, there is an + * xl_multi_insert_tuple struct, followed by the tuple data for each tuple. + * There is padding to align each xl_multi_insert_tuple struct. */ typedef struct xl_heap_multi_insert { @@ -201,7 +208,7 @@ typedef struct xl_multi_insert_tuple /* * This is what we need to know about update|hot_update * - * Backup blk 0: new page + * HEAP_UPDATE_BLKREF_HEAP_NEW: new page * * If XLH_UPDATE_PREFIX_FROM_OLD or XLH_UPDATE_SUFFIX_FROM_OLD flags are set, * the prefix and/or suffix come first, as one or two uint16s. @@ -213,8 +220,13 @@ typedef struct xl_multi_insert_tuple * If XLH_UPDATE_CONTAINS_NEW_TUPLE flag is given, the tuple data is * included even if a full-page image was taken. * - * Backup blk 1: old page, if different. (no data, just a reference to the blk) + * HEAP_UPDATE_BLKREF_HEAP_OLD: old page, if different. (no data, just a reference + * to the block) */ + +#define HEAP_UPDATE_BLKREF_HEAP_NEW 0 +#define HEAP_UPDATE_BLKREF_HEAP_OLD 1 + typedef struct xl_heap_update { TransactionId old_xmax; /* xmax of the old tuple */ @@ -393,6 +405,8 @@ typedef struct xlhp_prune_items #define XLH_LOCK_ALL_FROZEN_CLEARED 0x01 /* This is what we need to know about lock */ +#define HEAP_LOCK_BLKREF_HEAP 0 + typedef struct xl_heap_lock { TransactionId xmax; /* might be a MultiXactId */ From f581fa729d8e108fef853c3156267b1f753d0210 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Wed, 15 Jul 2026 17:37:34 -0400 Subject: [PATCH 146/250] Fix VM clear WAL logging by registering VM blocks Heap WAL records that clear bits on the visibility map (like inserts and deletes) did not register the visibility map blocks they modified. Because the WAL summarizer only records registered blocks, an incremental backup taken over such operations would omit the changed VM pages. On restore, the VM would retain stale all-visible/all-frozen bits, which can cause wrong results from index-only scans and incorrect relfrozenxid advancement due to vacuum page skipping. Not registering the VM buffer also meant we never emitted FPIs of VM pages when clearing bits. A torn VM page won't raise an error because the VM is read with ZERO_ON_ERROR; with checksums on, it would be detected and zeroed, but with checksums off, it is accepted as-is and can lead to data corruption. Fix this by registering the VM buffer in the WAL record when clearing VM bits. The VM buffer must now be locked throughout the critical section that modifies the VM and heap pages and emits the WAL record. This can slow down operations that clear the VM, since the VM lock is held longer and VM FPIs may be emitted, but it is required for correctness. Note that this fix does not repair existing incremental backups. Author: Melanie Plageman Author: Andres Freund Reviewed-by: Robert Haas Reviewed-by: Andrey Borodin Discussion: https://postgr.es/m/flat/CAAKRu_bn%2Be7F4yPFBgFbnP%2BsyJRKyNK092bjD2LKvZW7O4Svag> Backpatch-through: 17 --- contrib/pg_surgery/heap_surgery.c | 40 ++- src/backend/access/heap/heapam.c | 397 ++++++++++++++++++++---- src/backend/access/heap/heapam_xlog.c | 252 ++++++++++----- src/backend/access/heap/visibilitymap.c | 34 +- src/bin/pg_walsummary/t/002_blocks.pl | 7 +- src/include/access/heapam_xlog.h | 18 +- src/include/access/visibilitymap.h | 2 + 7 files changed, 595 insertions(+), 155 deletions(-) diff --git a/contrib/pg_surgery/heap_surgery.c b/contrib/pg_surgery/heap_surgery.c index 602aca66c60..6a38ac577c6 100644 --- a/contrib/pg_surgery/heap_surgery.c +++ b/contrib/pg_surgery/heap_surgery.c @@ -17,6 +17,7 @@ #include "access/visibilitymap.h" #include "access/xloginsert.h" #include "catalog/pg_am_d.h" +#include "catalog/pg_control.h" #include "miscadmin.h" #include "storage/bufmgr.h" #include "utils/acl.h" @@ -146,6 +147,7 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) { Buffer buf; Buffer vmbuf = InvalidBuffer; + bool unlock_vmbuf = false; Page page; BlockNumber blkno; OffsetNumber curoff; @@ -233,11 +235,15 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) } /* - * Before entering the critical section, pin the visibility map page - * if it appears to be necessary. + * Before entering the critical section, pin and lock the visibility + * map page if it appears to be necessary. */ if (heap_force_opt == HEAP_FORCE_KILL && PageIsAllVisible(page)) + { visibilitymap_pin(rel, blkno, &vmbuf); + LockBuffer(vmbuf, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuf = true; + } /* No ereport(ERROR) from here until all the changes are logged. */ START_CRIT_SECTION(); @@ -266,10 +272,11 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) */ if (PageIsAllVisible(page)) { + if (visibilitymap_clear_locked(rel, blkno, vmbuf, + VISIBILITYMAP_VALID_BITS)) + did_modify_vm = true; + PageClearAllVisible(page); - visibilitymap_clear(rel, blkno, vmbuf, - VISIBILITYMAP_VALID_BITS); - did_modify_vm = true; } } else @@ -320,18 +327,29 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) /* XLOG stuff */ if (RelationNeedsWAL(rel)) - log_newpage_buffer(buf, true); + { + XLogRecPtr recptr; + + XLogBeginInsert(); + XLogRegisterBuffer(0, buf, REGBUF_STANDARD | REGBUF_FORCE_IMAGE); + /* Include the VM page if it was modified. */ + if (did_modify_vm) + XLogRegisterBuffer(1, vmbuf, REGBUF_FORCE_IMAGE); + recptr = XLogInsert(RM_XLOG_ID, XLOG_FPI); + if (did_modify_vm) + PageSetLSN(BufferGetPage(vmbuf), recptr); + PageSetLSN(BufferGetPage(buf), recptr); + } } - /* WAL log the VM page if it was modified. */ - if (did_modify_vm && RelationNeedsWAL(rel)) - log_newpage_buffer(vmbuf, false); - END_CRIT_SECTION(); UnlockReleaseBuffer(buf); - if (vmbuf != InvalidBuffer) + if (unlock_vmbuf) + LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK); + + if (BufferIsValid(vmbuf)) ReleaseBuffer(vmbuf); /* Update the current_start_ptr before moving to the next page. */ diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index cf37956a2bf..d72b41ef92f 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -58,7 +58,8 @@ static HeapTuple heap_prepare_insert(Relation relation, HeapTuple tup, TransactionId xid, CommandId cid, int options); static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf, - Buffer newbuf, HeapTuple oldtup, + Buffer vmbuffer_old, Buffer newbuf, + Buffer vmbuffer_new, HeapTuple oldtup, HeapTuple newtup, HeapTuple old_key_tuple, bool all_visible_cleared, bool new_all_visible_cleared); #ifdef USE_ASSERT_CHECKING @@ -2083,8 +2084,10 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, TransactionId xid = GetCurrentTransactionId(); HeapTuple heaptup; Buffer buffer; + Page page; Buffer vmbuffer = InvalidBuffer; - bool all_visible_cleared = false; + bool clear_all_visible = false; + bool vmbuffer_modified = false; /* Cheap, simplistic check that the tuple matches the rel's rowtype. */ Assert(HeapTupleHeaderGetNatts(tup->t_data) <= @@ -2108,6 +2111,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, InvalidBuffer, options, bistate, &vmbuffer, NULL, 0); + page = BufferGetPage(buffer); /* * We're about to do the actual insert -- but check for conflict first, to @@ -2126,19 +2130,28 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, */ CheckForSerializableConflictIn(relation, NULL, InvalidBlockNumber); + /* Lock the vmbuffer before the critical section */ + if (PageIsAllVisible(page)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + clear_all_visible = true; + } + /* NO EREPORT(ERROR) from here till changes are logged */ START_CRIT_SECTION(); RelationPutHeapTuple(relation, buffer, heaptup, (options & HEAP_INSERT_SPECULATIVE) != 0); - if (PageIsAllVisible(BufferGetPage(buffer))) + if (clear_all_visible) { - all_visible_cleared = true; - PageClearAllVisible(BufferGetPage(buffer)); - visibilitymap_clear(relation, - ItemPointerGetBlockNumber(&(heaptup->t_self)), - vmbuffer, VISIBILITYMAP_VALID_BITS); + /* It's possible the VM bits were already clear */ + if (visibilitymap_clear_locked(relation, + ItemPointerGetBlockNumber(&(heaptup->t_self)), + vmbuffer, VISIBILITYMAP_VALID_BITS)) + vmbuffer_modified = true; + + PageClearAllVisible(page); } /* @@ -2160,7 +2173,6 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, xl_heap_insert xlrec; xl_heap_header xlhdr; XLogRecPtr recptr; - Page page = BufferGetPage(buffer); uint8 info = XLOG_HEAP_INSERT; int bufflags = 0; @@ -2185,7 +2197,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, xlrec.offnum = ItemPointerGetOffsetNumber(&heaptup->t_self); xlrec.flags = 0; - if (all_visible_cleared) + if (clear_all_visible) xlrec.flags |= XLH_INSERT_ALL_VISIBLE_CLEARED; if (options & HEAP_INSERT_SPECULATIVE) xlrec.flags |= XLH_INSERT_IS_SPECULATIVE; @@ -2230,15 +2242,28 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, /* filtering by origin on a row level is much more efficient */ XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN); + if (vmbuffer_modified) + XLogRegisterBuffer(HEAP_INSERT_BLKREF_VM, vmbuffer, 0); + recptr = XLogInsert(RM_HEAP_ID, info); PageSetLSN(page, recptr); + + if (vmbuffer_modified) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); UnlockReleaseBuffer(buffer); - if (vmbuffer != InvalidBuffer) + + /* + * We locked vmbuffer if clear_all_visible was true regardless of whether + * or not we ended up modifying the vmbuffer. + */ + if (clear_all_visible) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + if (BufferIsValid(vmbuffer)) ReleaseBuffer(vmbuffer); /* @@ -2419,8 +2444,9 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, while (ndone < ntuples) { Buffer buffer; - bool all_visible_cleared = false; + bool clear_all_visible = false; bool all_frozen_set = false; + bool vmbuffer_modified = false; int nthispage; CHECK_FOR_INTERRUPTS(); @@ -2462,6 +2488,17 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, if (starting_with_empty_page && (options & HEAP_INSERT_FROZEN)) all_frozen_set = true; + /* + * If clearing all-visible, take the VM buffer lock before entering + * the critical section where that action will be WAL-logged. Setting + * the VM all-frozen is done and WAL-logged separately. + */ + if (PageIsAllVisible(page) && !(options & HEAP_INSERT_FROZEN)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + clear_all_visible = true; + } + /* NO EREPORT(ERROR) from here till changes are logged */ START_CRIT_SECTION(); @@ -2502,13 +2539,16 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, * If we're only adding already frozen rows to a previously empty * page, mark it as all-visible. */ - if (PageIsAllVisible(page) && !(options & HEAP_INSERT_FROZEN)) + if (clear_all_visible) { - all_visible_cleared = true; + Assert(!(options & HEAP_INSERT_FROZEN)); + /* It's possible the VM bits were already clear */ + if (visibilitymap_clear_locked(relation, + BufferGetBlockNumber(buffer), + vmbuffer, VISIBILITYMAP_VALID_BITS)) + vmbuffer_modified = true; + PageClearAllVisible(page); - visibilitymap_clear(relation, - BufferGetBlockNumber(buffer), - vmbuffer, VISIBILITYMAP_VALID_BITS); } else if (all_frozen_set) PageSetAllVisible(page); @@ -2554,10 +2594,10 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, tupledata = scratchptr; /* check that the mutually exclusive flags are not both set */ - Assert(!(all_visible_cleared && all_frozen_set)); + Assert(!(clear_all_visible && all_frozen_set)); xlrec->flags = 0; - if (all_visible_cleared) + if (clear_all_visible) xlrec->flags = XLH_INSERT_ALL_VISIBLE_CLEARED; if (all_frozen_set) xlrec->flags = XLH_INSERT_ALL_FROZEN_SET; @@ -2623,6 +2663,8 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, XLogRegisterData(xlrec, tupledata - scratch.data); XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_HEAP, buffer, REGBUF_STANDARD | bufflags); + if (vmbuffer_modified) + XLogRegisterBuffer(HEAP_MULTI_INSERT_BLKREF_VM, vmbuffer, 0); XLogRegisterBufData(HEAP_MULTI_INSERT_BLKREF_HEAP, tupledata, totaldatalen); @@ -2633,10 +2675,19 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, recptr = XLogInsert(RM_HEAP2_ID, info); PageSetLSN(page, recptr); + if (vmbuffer_modified) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* + * We locked vmbuffer if clear_all_visible was true regardless of + * whether or not we ended up modifying the vmbuffer. + */ + if (clear_all_visible) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + /* * If we've frozen everything on the page, update the visibilitymap. * We're already holding pin on the vmbuffer. @@ -2786,12 +2837,13 @@ heap_delete(Relation relation, ItemPointer tid, BlockNumber block; Buffer buffer; Buffer vmbuffer = InvalidBuffer; + bool vmbuffer_modified = false; TransactionId new_xmax; uint16 new_infomask, new_infomask2; bool have_tuple_lock = false; bool iscombo; - bool all_visible_cleared = false; + bool clear_all_visible = false; HeapTuple old_key_tuple = NULL; /* replica identity of the tuple */ bool old_key_copied = false; @@ -3040,6 +3092,13 @@ heap_delete(Relation relation, ItemPointer tid, xid, LockTupleExclusive, true, &new_xmax, &new_infomask, &new_infomask2); + /* Lock the VM before entering the critical section */ + if (PageIsAllVisible(page)) + { + clear_all_visible = true; + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + } + START_CRIT_SECTION(); /* @@ -3051,12 +3110,14 @@ heap_delete(Relation relation, ItemPointer tid, */ PageSetPrunable(page, xid); - if (PageIsAllVisible(page)) + if (clear_all_visible) { - all_visible_cleared = true; + /* It's possible the VM bits were already clear */ + if (visibilitymap_clear_locked(relation, BufferGetBlockNumber(buffer), + vmbuffer, VISIBILITYMAP_VALID_BITS)) + vmbuffer_modified = true; + PageClearAllVisible(page); - visibilitymap_clear(relation, BufferGetBlockNumber(buffer), - vmbuffer, VISIBILITYMAP_VALID_BITS); } /* store transaction information of xact deleting the tuple */ @@ -3096,7 +3157,7 @@ heap_delete(Relation relation, ItemPointer tid, log_heap_new_cid(relation, &tp); xlrec.flags = 0; - if (all_visible_cleared) + if (clear_all_visible) xlrec.flags |= XLH_DELETE_ALL_VISIBLE_CLEARED; if (changingPart) xlrec.flags |= XLH_DELETE_IS_PARTITION_MOVE; @@ -3137,13 +3198,27 @@ heap_delete(Relation relation, ItemPointer tid, /* filtering by origin on a row level is much more efficient */ XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN); + if (vmbuffer_modified) + XLogRegisterBuffer(HEAP_DELETE_BLKREF_VM, vmbuffer, 0); + recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_DELETE); PageSetLSN(page, recptr); + + if (vmbuffer_modified) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* + * Release VM lock first, since it covers many heap blocks. We locked + * vmbuffer if clear_all_visible was true regardless of whether or not we + * ended up modifying the vmbuffer. + */ + if (clear_all_visible) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + LockBuffer(buffer, BUFFER_LOCK_UNLOCK); if (vmbuffer != InvalidBuffer) @@ -3261,13 +3336,16 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, HeapTuple heaptup; HeapTuple old_key_tuple = NULL; bool old_key_copied = false; - Page page; + Page page, + newpage; BlockNumber block; MultiXactStatus mxact_status; Buffer buffer, newbuf, vmbuffer = InvalidBuffer, vmbuffer_new = InvalidBuffer; + bool unlock_vmbuffer = false; + bool unlock_vmbuffer_new = false; bool need_toast; Size newtupsize, pagefree; @@ -3276,8 +3354,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, bool use_hot_update = false; bool summarized_update = false; bool key_intact; - bool all_visible_cleared = false; - bool all_visible_cleared_new = false; + bool clear_all_visible = false; + bool clear_all_visible_new = false; + bool vmbuffer_modified = false; + bool vmbuffer_new_modified = false; bool checked_lockers; bool locker_remains; bool id_has_external = false; @@ -3852,6 +3932,12 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, Assert(HEAP_XMAX_IS_LOCKED_ONLY(infomask_lock_old_tuple)); + if (PageIsAllVisible(page)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuffer = true; + } + START_CRIT_SECTION(); /* Clear obsolete visibility flags ... */ @@ -3874,10 +3960,13 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, * overhead would be unchanged, that doesn't seem necessarily * worthwhile. */ - if (PageIsAllVisible(page) && - visibilitymap_clear(relation, block, vmbuffer, - VISIBILITYMAP_ALL_FROZEN)) - cleared_all_frozen = true; + if (PageIsAllVisible(page)) + { + /* It's possible all-frozen was already clear */ + if (visibilitymap_clear_locked(relation, block, vmbuffer, + VISIBILITYMAP_ALL_FROZEN)) + cleared_all_frozen = true; + } MarkBufferDirty(buffer); @@ -3896,12 +3985,24 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, xlrec.flags = cleared_all_frozen ? XLH_LOCK_ALL_FROZEN_CLEARED : 0; XLogRegisterData(&xlrec, SizeOfHeapLock); + + if (cleared_all_frozen) + XLogRegisterBuffer(HEAP_LOCK_BLKREF_VM, vmbuffer, 0); + recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_LOCK); PageSetLSN(page, recptr); + + if (cleared_all_frozen) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* release VM lock first, since it covers many heap blocks */ + if (unlock_vmbuffer) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + unlock_vmbuffer = false; + LockBuffer(buffer, BUFFER_LOCK_UNLOCK); /* @@ -3987,6 +4088,8 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, heaptup = newtup; } + newpage = BufferGetPage(newbuf); + /* * We're about to do the actual update -- check for conflict first, to * avoid possibly having to roll back work we've just done. @@ -4050,6 +4153,69 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, id_has_external, &old_key_copied); + clear_all_visible = PageIsAllVisible(page); + clear_all_visible_new = newbuf != buffer && PageIsAllVisible(newpage); + + /* + * Clear PD_ALL_VISIBLE flags and reset visibility map bits for any heap + * pages that were all-visible. If there are two heap pages, we may need + * to clear VM bits for both. + */ + if (clear_all_visible && clear_all_visible_new && + vmbuffer_new == vmbuffer) + { + /* + * This is the more complicated case: both the new and old heap pages + * are all-visible and both their VM bits are on the same page of the + * VM, so we register a single VM buffer as HEAP_UPDATE_BLKREF_VM_NEW + * in the WAL record. We must be careful to only lock and register one + * buffer, even though we modify it twice -- once for each heap + * block's VM bits. + */ + LockBuffer(vmbuffer_new, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuffer_new = true; + + /* We will not lock or attempt to modify old VM buffer */ + } + else + { + /* + * In all the remaining cases, we will clear at most one heap block's + * VM bits per VM page. + */ + Buffer vmbuffers[2] = { + clear_all_visible ? vmbuffer : InvalidBuffer, + clear_all_visible_new ? vmbuffer_new : InvalidBuffer + }; + + /* + * When both pages need different VM pages cleared, acquire the VM + * buffer locks in VM block order to avoid deadlocks between backends + * updating tuples in opposite directions across VM pages. + */ + if (clear_all_visible && clear_all_visible_new && + BufferGetBlockNumber(vmbuffers[0]) > BufferGetBlockNumber(vmbuffers[1])) + { + Buffer swap = vmbuffers[0]; + + vmbuffers[0] = vmbuffers[1]; + vmbuffers[1] = swap; + } + + Assert((!BufferIsValid(vmbuffers[0]) && !BufferIsValid(vmbuffers[1])) || + vmbuffers[0] != vmbuffers[1]); + + if (BufferIsValid(vmbuffers[0])) + LockBuffer(vmbuffers[0], BUFFER_LOCK_EXCLUSIVE); + if (BufferIsValid(vmbuffers[1])) + LockBuffer(vmbuffers[1], BUFFER_LOCK_EXCLUSIVE); + + if (clear_all_visible) + unlock_vmbuffer = true; + if (clear_all_visible_new) + unlock_vmbuffer_new = true; + } + /* NO EREPORT(ERROR) from here till changes are logged */ START_CRIT_SECTION(); @@ -4086,7 +4252,6 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, RelationPutHeapTuple(relation, newbuf, heaptup, false); /* insert new tuple */ - /* Clear obsolete visibility flags, possibly set by ourselves above... */ oldtup.t_data->t_infomask &= ~(HEAP_XMAX_BITS | HEAP_MOVED); oldtup.t_data->t_infomask2 &= ~HEAP_KEYS_UPDATED; @@ -4100,20 +4265,42 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, /* record address of new tuple in t_ctid of old one */ oldtup.t_data->t_ctid = heaptup->t_self; - /* clear PD_ALL_VISIBLE flags, reset all visibilitymap bits */ - if (PageIsAllVisible(BufferGetPage(buffer))) + /* + * Clear PD_ALL_VISIBLE flags and reset all visibilitymap bits. In all + * cases, it's possible that PD_ALL_VISIBLE was set but the corresponding + * visibility map bits were already clear. + */ + if (clear_all_visible) { - all_visible_cleared = true; - PageClearAllVisible(BufferGetPage(buffer)); - visibilitymap_clear(relation, BufferGetBlockNumber(buffer), - vmbuffer, VISIBILITYMAP_VALID_BITS); + if (visibilitymap_clear_locked(relation, block, + vmbuffer, VISIBILITYMAP_VALID_BITS)) + { + /* + * When old and new heap blocks' VM bits are on the same VM page, + * that page is registered in the WAL record only once. If both + * heap pages were PD_ALL_VISIBLE and either VM bit needs + * clearing, we register the VM buffer as + * HEAP_UPDATE_BLKREF_VM_NEW. + */ + if (clear_all_visible_new && vmbuffer == vmbuffer_new) + vmbuffer_new_modified = true; + else + vmbuffer_modified = true; + } + + PageClearAllVisible(page); } - if (newbuf != buffer && PageIsAllVisible(BufferGetPage(newbuf))) + if (clear_all_visible_new) { - all_visible_cleared_new = true; - PageClearAllVisible(BufferGetPage(newbuf)); - visibilitymap_clear(relation, BufferGetBlockNumber(newbuf), - vmbuffer_new, VISIBILITYMAP_VALID_BITS); + /* + * If both heap blocks' VM bits are on the same VM buffer, this will + * clear the new heap block's VM bits from the shared vmbuffer. + */ + if (visibilitymap_clear_locked(relation, BufferGetBlockNumber(newbuf), + vmbuffer_new, VISIBILITYMAP_VALID_BITS)) + vmbuffer_new_modified = true; + + PageClearAllVisible(newpage); } if (newbuf != buffer) @@ -4136,19 +4323,30 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, } recptr = log_heap_update(relation, buffer, - newbuf, &oldtup, heaptup, + vmbuffer_modified ? vmbuffer : InvalidBuffer, + newbuf, + vmbuffer_new_modified ? vmbuffer_new : InvalidBuffer, + &oldtup, heaptup, old_key_tuple, - all_visible_cleared, - all_visible_cleared_new); + clear_all_visible, + clear_all_visible_new); if (newbuf != buffer) - { - PageSetLSN(BufferGetPage(newbuf), recptr); - } + PageSetLSN(newpage, recptr); PageSetLSN(BufferGetPage(buffer), recptr); + + if (vmbuffer_modified) + PageSetLSN(BufferGetPage(vmbuffer), recptr); + if (vmbuffer_new_modified) + PageSetLSN(BufferGetPage(vmbuffer_new), recptr); } END_CRIT_SECTION(); + if (unlock_vmbuffer) + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + if (unlock_vmbuffer_new) + LockBuffer(vmbuffer_new, BUFFER_LOCK_UNLOCK); + if (newbuf != buffer) LockBuffer(newbuf, BUFFER_LOCK_UNLOCK); LockBuffer(buffer, BUFFER_LOCK_UNLOCK); @@ -4586,6 +4784,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, ItemId lp; Page page; Buffer vmbuffer = InvalidBuffer; + bool unlock_vmbuffer = false; BlockNumber block; TransactionId xid, xmax; @@ -4599,6 +4798,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, *buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(tid)); block = ItemPointerGetBlockNumber(tid); + page = BufferGetPage(*buffer); /* * Before locking the buffer, pin the visibility map page if it appears to @@ -4606,12 +4806,11 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, * in the middle of changing this, so we'll need to recheck after we have * the lock. */ - if (PageIsAllVisible(BufferGetPage(*buffer))) + if (PageIsAllVisible(page)) visibilitymap_pin(relation, block, &vmbuffer); LockBuffer(*buffer, BUFFER_LOCK_EXCLUSIVE); - page = BufferGetPage(*buffer); lp = PageGetItemId(page, ItemPointerGetOffsetNumber(tid)); Assert(ItemIdIsNormal(lp)); @@ -5166,6 +5365,13 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, GetCurrentTransactionId(), mode, false, &xid, &new_infomask, &new_infomask2); + /* Lock VM buffer before entering critical section */ + if (PageIsAllVisible(page)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuffer = true; + } + START_CRIT_SECTION(); /* @@ -5197,11 +5403,13 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, tuple->t_data->t_ctid = *tid; /* Clear only the all-frozen bit on visibility map if needed */ - if (PageIsAllVisible(page) && - visibilitymap_clear(relation, block, vmbuffer, - VISIBILITYMAP_ALL_FROZEN)) - cleared_all_frozen = true; - + if (PageIsAllVisible(page)) + { + /* It's possible all-frozen was already clear */ + if (visibilitymap_clear_locked(relation, block, vmbuffer, + VISIBILITYMAP_ALL_FROZEN)) + cleared_all_frozen = true; + } MarkBufferDirty(*buffer); @@ -5232,19 +5440,33 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, xlrec.flags = cleared_all_frozen ? XLH_LOCK_ALL_FROZEN_CLEARED : 0; XLogRegisterData(&xlrec, SizeOfHeapLock); + if (cleared_all_frozen) + XLogRegisterBuffer(HEAP_LOCK_BLKREF_VM, vmbuffer, 0); + /* we don't decode row locks atm, so no need to log the origin */ recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_LOCK); PageSetLSN(page, recptr); + + if (cleared_all_frozen) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* release VM lock first, since it covers many heap blocks */ + if (unlock_vmbuffer) + { + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + unlock_vmbuffer = false; + } + result = TM_Ok; out_locked: LockBuffer(*buffer, BUFFER_LOCK_UNLOCK); + Assert(!unlock_vmbuffer); out_unlocked: if (BufferIsValid(vmbuffer)) @@ -5707,6 +5929,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, ItemPointerData tupid; HeapTupleData mytup; Buffer buf; + Page page; uint16 new_infomask, new_infomask2, old_infomask, @@ -5716,6 +5939,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, bool cleared_all_frozen = false; bool pinned_desired_page; Buffer vmbuffer = InvalidBuffer; + bool unlock_vmbuffer = false; BlockNumber block; ItemPointerCopy(tid, &tupid); @@ -5724,6 +5948,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, { new_infomask = 0; new_xmax = InvalidTransactionId; + cleared_all_frozen = false; block = ItemPointerGetBlockNumber(&tupid); ItemPointerCopy(&tupid, &(mytup.t_self)); @@ -5743,13 +5968,15 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, l4: CHECK_FOR_INTERRUPTS(); + page = BufferGetPage(buf); + /* * Before locking the buffer, pin the visibility map page if it * appears to be necessary. Since we haven't got the lock yet, * someone else might be in the middle of changing this, so we'll need * to recheck after we have the lock. */ - if (PageIsAllVisible(BufferGetPage(buf))) + if (PageIsAllVisible(page)) { visibilitymap_pin(rel, block, &vmbuffer); pinned_desired_page = true; @@ -5770,7 +5997,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, * this page. If this page isn't all-visible, we won't use the vm * page, but we hold onto such a pin till the end of the function. */ - if (!pinned_desired_page && PageIsAllVisible(BufferGetPage(buf))) + if (!pinned_desired_page && PageIsAllVisible(page)) { LockBuffer(buf, BUFFER_LOCK_UNLOCK); visibilitymap_pin(rel, block, &vmbuffer); @@ -5951,10 +6178,11 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, xid, mode, false, &new_xmax, &new_infomask, &new_infomask2); - if (PageIsAllVisible(BufferGetPage(buf)) && - visibilitymap_clear(rel, block, vmbuffer, - VISIBILITYMAP_ALL_FROZEN)) - cleared_all_frozen = true; + if (PageIsAllVisible(page)) + { + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + unlock_vmbuffer = true; + } START_CRIT_SECTION(); @@ -5967,12 +6195,19 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, MarkBufferDirty(buf); + if (PageIsAllVisible(page)) + { + /* It's possible all-frozen was already clear */ + if (visibilitymap_clear_locked(rel, block, vmbuffer, + VISIBILITYMAP_ALL_FROZEN)) + cleared_all_frozen = true; + } + /* XLOG stuff */ if (RelationNeedsWAL(rel)) { xl_heap_lock_updated xlrec; XLogRecPtr recptr; - Page page = BufferGetPage(buf); XLogBeginInsert(); XLogRegisterBuffer(HEAP_LOCK_BLKREF_HEAP, buf, REGBUF_STANDARD); @@ -5985,13 +6220,26 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, XLogRegisterData(&xlrec, SizeOfHeapLockUpdated); + if (cleared_all_frozen) + XLogRegisterBuffer(HEAP_LOCK_BLKREF_VM, vmbuffer, 0); + recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_LOCK_UPDATED); PageSetLSN(page, recptr); + + if (cleared_all_frozen) + PageSetLSN(BufferGetPage(vmbuffer), recptr); } END_CRIT_SECTION(); + /* release VM lock first, since it covers many heap blocks */ + if (unlock_vmbuffer) + { + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + unlock_vmbuffer = false; + } + next: /* if we find the end of update chain, we're done. */ if (mytup.t_data->t_infomask & HEAP_XMAX_INVALID || @@ -6017,6 +6265,7 @@ heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, out_unlocked: if (vmbuffer != InvalidBuffer) ReleaseBuffer(vmbuffer); + Assert(!unlock_vmbuffer); return result; } @@ -8848,8 +9097,9 @@ log_heap_visible(Relation rel, Buffer heap_buffer, Buffer vm_buffer, * have modified the buffer(s) and marked them dirty. */ static XLogRecPtr -log_heap_update(Relation reln, Buffer oldbuf, - Buffer newbuf, HeapTuple oldtup, HeapTuple newtup, +log_heap_update(Relation reln, Buffer oldbuf, Buffer vmbuffer_old, + Buffer newbuf, Buffer vmbuffer_new, + HeapTuple oldtup, HeapTuple newtup, HeapTuple old_key_tuple, bool all_visible_cleared, bool new_all_visible_cleared) { @@ -9058,6 +9308,21 @@ log_heap_update(Relation reln, Buffer oldbuf, old_key_tuple->t_len - SizeofHeapTupleHeader); } + /* + * Register VM buffers. If the old and new heap pages' VM bits are on the + * same VM page and both their VM bits were cleared, the caller passes + * only vmbuffer_new (mirroring the heap page convention where block 0 = + * new is always registered). + */ + Assert((BufferIsInvalid(vmbuffer_old) && BufferIsInvalid(vmbuffer_new)) || + (vmbuffer_old != vmbuffer_new)); + + if (BufferIsValid(vmbuffer_new)) + XLogRegisterBuffer(HEAP_UPDATE_BLKREF_VM_NEW, vmbuffer_new, 0); + + if (BufferIsValid(vmbuffer_old)) + XLogRegisterBuffer(HEAP_UPDATE_BLKREF_VM_OLD, vmbuffer_old, 0); + /* filtering by origin on a row level is much more efficient */ XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN); diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c index 87b5c44fd46..0ce23dedfe4 100644 --- a/src/backend/access/heap/heapam_xlog.c +++ b/src/backend/access/heap/heapam_xlog.c @@ -22,6 +22,70 @@ #include "storage/freespace.h" #include "storage/standby.h" +/* + * Clear visibility map bits for a single heap block during heap redo. + * + * Used by records that modify one heap block and, at most, its corresponding + * VM block (insert, delete, multi_insert, lock). Records that can touch + * multiple heap or VM blocks (e.g. updates) replay the VM changes inline + * instead. + * + * 'record' is the WAL record being replayed + * 'target_locator' identifies the relation whose VM is being updated + * 'heap_blkno' is the heap block whose VM bits should be cleared + * 'wal_vm_block_id' is the WAL block reference id of the VM page + * 'flags' specifies which visibility map bits to clear + */ +static void +heap_xlog_vm_clear(XLogReaderState *record, + RelFileLocator target_locator, + BlockNumber heap_blkno, + uint8 wal_vm_block_id, uint8 flags) +{ + XLogRecPtr lsn = record->EndRecPtr; + Relation reln = CreateFakeRelcacheEntry(target_locator); + Buffer vmbuffer = InvalidBuffer; + + /* + * If the vmbuffer was registered, use the recovery-specific routines to + * read it. These will either apply an FPI or indicate that we should + * clear the requested bits ourselves. + * + * Originally, clearing the VM did not register the VM buffers, so since + * registering the VM buffer was a bug fix, we keep a fallback path to + * support replay of WAL generated from before the fix. + */ + if (XLogRecHasBlockRef(record, wal_vm_block_id)) + { + if (XLogReadBufferForRedo(record, wal_vm_block_id, + &vmbuffer) == BLK_NEEDS_REDO) + { + if (visibilitymap_clear_locked(reln, + heap_blkno, vmbuffer, + flags)) + PageSetLSN(BufferGetPage(vmbuffer), lsn); + } + if (BufferIsValid(vmbuffer)) + UnlockReleaseBuffer(vmbuffer); + } + else + { + /* + * This is the backwards compatibility path to clear VM bits for + * records predating VM buffer registration. It is also invoked if the + * heap page's PD_ALL_VISIBLE was cleared but the VM bits were already + * clear. The WAL record flags do not distinguish between these two + * situations. Though this is wasted effort, the behavior is + * historical and the situation should be rare. + */ + visibilitymap_pin(reln, heap_blkno, &vmbuffer); + visibilitymap_clear(reln, heap_blkno, vmbuffer, flags); + ReleaseBuffer(vmbuffer); + } + + FreeFakeRelcacheEntry(reln); +} + /* * Replay XLOG_HEAP2_PRUNE_* records. @@ -360,15 +424,9 @@ heap_xlog_delete(XLogReaderState *record) * already up-to-date. */ if (xlrec->flags & XLH_DELETE_ALL_VISIBLE_CLEARED) - { - Relation reln = CreateFakeRelcacheEntry(target_locator); - Buffer vmbuffer = InvalidBuffer; - - visibilitymap_pin(reln, blkno, &vmbuffer); - visibilitymap_clear(reln, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); - } + heap_xlog_vm_clear(record, target_locator, + blkno, HEAP_DELETE_BLKREF_VM, + VISIBILITYMAP_VALID_BITS); if (XLogReadBufferForRedo(record, HEAP_DELETE_BLKREF_HEAP, &buffer) == BLK_NEEDS_REDO) @@ -449,15 +507,9 @@ heap_xlog_insert(XLogReaderState *record) * already up-to-date. */ if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) - { - Relation reln = CreateFakeRelcacheEntry(target_locator); - Buffer vmbuffer = InvalidBuffer; - - visibilitymap_pin(reln, blkno, &vmbuffer); - visibilitymap_clear(reln, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); - } + heap_xlog_vm_clear(record, target_locator, + blkno, HEAP_INSERT_BLKREF_VM, + VISIBILITYMAP_VALID_BITS); /* * If we inserted the first and only tuple on the page, re-initialize the @@ -573,17 +625,15 @@ heap_xlog_multi_insert(XLogReaderState *record) /* * The visibility map may need to be fixed even if the heap page is * already up-to-date. + * + * Clear the VM (if needed) before clearing the heap page-level visibility + * flag (PD_ALL_VISIBLE) to prevent the heap page from being marked + * all-visible in the VM while its PD_ALL_VISIBLE is clear. */ if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) - { - Relation reln = CreateFakeRelcacheEntry(rlocator); - Buffer vmbuffer = InvalidBuffer; - - visibilitymap_pin(reln, blkno, &vmbuffer); - visibilitymap_clear(reln, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); - } + heap_xlog_vm_clear(record, rlocator, + blkno, HEAP_MULTI_INSERT_BLKREF_VM, + VISIBILITYMAP_VALID_BITS); if (isinit) { @@ -698,6 +748,8 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) Buffer obuffer, nbuffer; Page page; + bool new_cleared, + old_cleared; OffsetNumber offnum; ItemId lp = NULL; HeapTupleData oldtup; @@ -737,14 +789,95 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) * The visibility map may need to be fixed even if the heap page is * already up-to-date. */ - if (xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED) + new_cleared = (xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED) != 0; + old_cleared = (xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED) != 0; + if (new_cleared || old_cleared) { Relation reln = CreateFakeRelcacheEntry(rlocator); - Buffer vmbuffer = InvalidBuffer; + bool has_vm_old = XLogRecHasBlockRef(record, HEAP_UPDATE_BLKREF_VM_OLD); + bool has_vm_new = XLogRecHasBlockRef(record, HEAP_UPDATE_BLKREF_VM_NEW); + + if (has_vm_new) + { + Buffer vmbuffer_new = InvalidBuffer; + + Assert(xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED); + + if (XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_VM_NEW, + &vmbuffer_new) == BLK_NEEDS_REDO) + { + /* + * If both the old and new heap pages were all-visible and + * their VM bits are on the same VM page, that single VM page + * is registered as HEAP_UPDATE_BLKREF_VM_NEW. Clear both heap + * blocks' VM bits from the single provided VM buffer. It's + * possible that one of the page's VM bits were already clear, + * but visibilitymap_clear() is harmless as long as we provide + * it the correct bits. + * + * We must verify that oldblk's VM bits really are on this VM + * page, rather than relying on the absence of a separate + * VM_OLD block reference: VM_OLD is also omitted when oldblk + * is on a different VM page but its bit was already clear. + */ + if (xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED && + visibilitymap_pin_ok(oldblk, vmbuffer_new)) + { + if (visibilitymap_clear_locked(reln, oldblk, vmbuffer_new, + VISIBILITYMAP_VALID_BITS)) + PageSetLSN(BufferGetPage(vmbuffer_new), lsn); + } + /* If VM_NEW is registered, we are sure newblk is on VM_NEW */ + if (visibilitymap_clear_locked(reln, newblk, vmbuffer_new, + VISIBILITYMAP_VALID_BITS)) + PageSetLSN(BufferGetPage(vmbuffer_new), lsn); + } + if (BufferIsValid(vmbuffer_new)) + UnlockReleaseBuffer(vmbuffer_new); + } + if (has_vm_old) + { + Buffer vmbuffer_old = InvalidBuffer; + + Assert(xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED); + + if (XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_VM_OLD, &vmbuffer_old) == + BLK_NEEDS_REDO) + { + if (visibilitymap_clear_locked(reln, oldblk, vmbuffer_old, + VISIBILITYMAP_VALID_BITS)) + PageSetLSN(BufferGetPage(vmbuffer_old), lsn); + } + if (BufferIsValid(vmbuffer_old)) + UnlockReleaseBuffer(vmbuffer_old); + } + if (!has_vm_old && !has_vm_new) + { + /* + * Backwards compatibility path. Previously, the VM buffers were + * not registered in the WAL record. We need this path to replay + * WAL generated by a not-yet-patched primary during upgrade. + */ + if (old_cleared) + { + Buffer vmbuffer = InvalidBuffer; + + visibilitymap_pin(reln, oldblk, &vmbuffer); + visibilitymap_clear(reln, oldblk, vmbuffer, + VISIBILITYMAP_VALID_BITS); + ReleaseBuffer(vmbuffer); + } + if (new_cleared) + { + Buffer vmbuffer = InvalidBuffer; + + visibilitymap_pin(reln, newblk, &vmbuffer); + visibilitymap_clear(reln, newblk, vmbuffer, + VISIBILITYMAP_VALID_BITS); + ReleaseBuffer(vmbuffer); + } + } - visibilitymap_pin(reln, oldblk, &vmbuffer); - visibilitymap_clear(reln, oldblk, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); FreeFakeRelcacheEntry(reln); } @@ -752,10 +885,12 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) * In normal operation, it is important to lock the two pages in * page-number order, to avoid possible deadlocks against other update * operations going the other way. However, during WAL replay there can - * be no other update happening, so we don't need to worry about that. But - * we *do* need to worry that we don't expose an inconsistent state to Hot - * Standby queries --- so the original page can't be unlocked before we've - * added the new tuple to the new page. + * be no other update happening, so we don't need to worry about that. + * Notice we also don't worry about this when locking VM buffers above. + * + * But we *do* need to worry that we don't expose an inconsistent state to + * Hot Standby queries --- so the original page can't be unlocked before + * we've added the new tuple to the new page. */ /* Deal with old tuple version */ @@ -793,7 +928,7 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) /* Mark the page as a candidate for pruning */ PageSetPrunable(page, XLogRecGetXid(record)); - if (xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED) + if (old_cleared) PageClearAllVisible(page); PageSetLSN(page, lsn); @@ -819,21 +954,6 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) newaction = XLogReadBufferForRedo(record, HEAP_UPDATE_BLKREF_HEAP_NEW, &nbuffer); - /* - * The visibility map may need to be fixed even if the heap page is - * already up-to-date. - */ - if (xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED) - { - Relation reln = CreateFakeRelcacheEntry(rlocator); - Buffer vmbuffer = InvalidBuffer; - - visibilitymap_pin(reln, newblk, &vmbuffer); - visibilitymap_clear(reln, newblk, vmbuffer, VISIBILITYMAP_VALID_BITS); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); - } - /* Deal with new tuple */ if (newaction == BLK_NEEDS_REDO) { @@ -930,7 +1050,7 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) if (offnum == InvalidOffsetNumber) elog(PANIC, "failed to add tuple"); - if (xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED) + if (new_cleared) PageClearAllVisible(page); freespace = PageGetHeapFreeSpace(page); /* needed to update FSM below */ @@ -1022,20 +1142,15 @@ heap_xlog_lock(XLogReaderState *record) */ if (xlrec->flags & XLH_LOCK_ALL_FROZEN_CLEARED) { - RelFileLocator rlocator; - Buffer vmbuffer = InvalidBuffer; BlockNumber block; - Relation reln; + RelFileLocator rlocator; XLogRecGetBlockTag(record, HEAP_LOCK_BLKREF_HEAP, &rlocator, NULL, &block); - reln = CreateFakeRelcacheEntry(rlocator); - - visibilitymap_pin(reln, block, &vmbuffer); - visibilitymap_clear(reln, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); + heap_xlog_vm_clear(record, rlocator, + block, HEAP_LOCK_BLKREF_VM, + VISIBILITYMAP_ALL_FROZEN); } if (XLogReadBufferForRedo(record, HEAP_LOCK_BLKREF_HEAP, @@ -1100,20 +1215,15 @@ heap_xlog_lock_updated(XLogReaderState *record) */ if (xlrec->flags & XLH_LOCK_ALL_FROZEN_CLEARED) { - RelFileLocator rlocator; - Buffer vmbuffer = InvalidBuffer; BlockNumber block; - Relation reln; + RelFileLocator rlocator; XLogRecGetBlockTag(record, HEAP_LOCK_BLKREF_HEAP, &rlocator, NULL, &block); - reln = CreateFakeRelcacheEntry(rlocator); - - visibilitymap_pin(reln, block, &vmbuffer); - visibilitymap_clear(reln, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN); - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); + heap_xlog_vm_clear(record, rlocator, + block, HEAP_LOCK_BLKREF_VM, + VISIBILITYMAP_ALL_FROZEN); } if (XLogReadBufferForRedo(record, HEAP_LOCK_BLKREF_HEAP, diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c index 1874a3fda37..d44620eca7f 100644 --- a/src/backend/access/heap/visibilitymap.c +++ b/src/backend/access/heap/visibilitymap.c @@ -135,9 +135,38 @@ static Buffer vm_extend(Relation rel, BlockNumber vm_nblocks); * You must pass a buffer containing the correct map page to this function. * Call visibilitymap_pin first to pin the right one. This function doesn't do * any I/O. Returns true if any bits have been cleared and false otherwise. + * + * Most callers should use visibilitymap_clear_locked rather than this + * function. It is usually necessary to register the VM buffer in the WAL + * record, and this necessitates holding the lock for longer than it is held + * here. However, we retain this function for behavioral compatibility on the + * back branches (out-of-tree callers may expect it to take the lock). And, + * since we have it, we use it for in-tree callers that don't have to manage + * the VM buffer lock themselves. */ bool visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags) +{ + bool cleared = false; + + LockBuffer(vmbuf, BUFFER_LOCK_EXCLUSIVE); + + cleared = visibilitymap_clear_locked(rel, heapBlk, vmbuf, flags); + + LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK); + + return cleared; +} + +/* + * Clear specified bits, caller holds VM lock + * + * Like visibilitymap_clear(), except the caller must already hold the VM + * buffer exclusive lock and is responsible for unlocking it. + * Returns true if any bits were actually cleared, false otherwise. + */ +bool +visibilitymap_clear_locked(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags) { BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk); int mapByte = HEAPBLK_TO_MAPBYTE(heapBlk); @@ -157,7 +186,8 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags if (!BufferIsValid(vmbuf) || BufferGetBlockNumber(vmbuf) != mapBlock) elog(ERROR, "wrong buffer passed to visibilitymap_clear"); - LockBuffer(vmbuf, BUFFER_LOCK_EXCLUSIVE); + Assert(BufferIsExclusiveLocked(vmbuf)); + map = PageGetContents(BufferGetPage(vmbuf)); if (map[mapByte] & mask) @@ -168,8 +198,6 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags cleared = true; } - LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK); - return cleared; } diff --git a/src/bin/pg_walsummary/t/002_blocks.pl b/src/bin/pg_walsummary/t/002_blocks.pl index 0f98c7df82e..fcb701e1b98 100644 --- a/src/bin/pg_walsummary/t/002_blocks.pl +++ b/src/bin/pg_walsummary/t/002_blocks.pl @@ -93,13 +93,14 @@ split(m@/@, $end_lsn); ok(-f $filename, "WAL summary file exists"); -# Run pg_walsummary on it. We expect exactly two blocks to be modified, -# block 0 and one other. +# Run pg_walsummary on it. We expect exactly three blocks to be modified, +# block 0 (old tuple), another block (new tuple), and the block for the VM. my ($stdout, $stderr) = run_command([ 'pg_walsummary', '-i', $filename ]); note($stdout); @lines = split(/\n/, $stdout); like($stdout, qr/FORK main: block 0$/m, "stdout shows block 0 modified"); +like($stdout, qr/FORK vm: block 0$/m, "stdout shows VM block 0 modified"); is($stderr, '', 'stderr is empty'); -is(0 + @lines, 2, "UPDATE modified 2 blocks"); +is(0 + @lines, 3, "UPDATE modified 3 blocks"); done_testing(); diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h index 92744c53899..5ec551e8d56 100644 --- a/src/include/access/heapam_xlog.h +++ b/src/include/access/heapam_xlog.h @@ -111,6 +111,7 @@ /* This is what we need to know about delete */ #define HEAP_DELETE_BLKREF_HEAP 0 +#define HEAP_DELETE_BLKREF_VM 1 typedef struct xl_heap_delete { @@ -160,6 +161,7 @@ typedef struct xl_heap_header /* This is what we need to know about insert */ #define HEAP_INSERT_BLKREF_HEAP 0 +#define HEAP_INSERT_BLKREF_VM 1 typedef struct xl_heap_insert { @@ -179,6 +181,7 @@ typedef struct xl_heap_insert * (XLOG_HEAP_INIT_PAGE). */ #define HEAP_MULTI_INSERT_BLKREF_HEAP 0 +#define HEAP_MULTI_INSERT_BLKREF_VM 1 /* * In HEAP_MULTI_INSERT_BLKREF_HEAP's data portion, there is an @@ -222,10 +225,22 @@ typedef struct xl_multi_insert_tuple * * HEAP_UPDATE_BLKREF_HEAP_OLD: old page, if different. (no data, just a reference * to the block) + * + * HEAP_UPDATE_BLKREF_VM_NEW: VM page covering the new heap page. Registered + * when XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED is set and the new heap page's VM bit + * was actually cleared. Also covers the old heap page's VM bits when both heap + * pages map to the same VM page and both blocks' VM bits were actually cleared. + * + * HEAP_UPDATE_BLKREF_VM_OLD: VM page covering the old heap page. Only + * registered when XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED is set and the old heap + * page's VM bits were actually cleared. Also only registered when the old heap + * page's VM bits are on a different VM page than the new heap page's or they + * are on the same VM page and only the old block's VM bits are cleared. */ - #define HEAP_UPDATE_BLKREF_HEAP_NEW 0 #define HEAP_UPDATE_BLKREF_HEAP_OLD 1 +#define HEAP_UPDATE_BLKREF_VM_NEW 2 +#define HEAP_UPDATE_BLKREF_VM_OLD 3 typedef struct xl_heap_update { @@ -406,6 +421,7 @@ typedef struct xlhp_prune_items /* This is what we need to know about lock */ #define HEAP_LOCK_BLKREF_HEAP 0 +#define HEAP_LOCK_BLKREF_VM 1 typedef struct xl_heap_lock { diff --git a/src/include/access/visibilitymap.h b/src/include/access/visibilitymap.h index ea889bf9ec7..1bc59c9ac33 100644 --- a/src/include/access/visibilitymap.h +++ b/src/include/access/visibilitymap.h @@ -26,6 +26,8 @@ #define VM_ALL_FROZEN(r, b, v) \ ((visibilitymap_get_status((r), (b), (v)) & VISIBILITYMAP_ALL_FROZEN) != 0) +extern bool visibilitymap_clear_locked(Relation rel, BlockNumber heapBlk, + Buffer vmbuf, uint8 flags); extern bool visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags); extern void visibilitymap_pin(Relation rel, BlockNumber heapBlk, From 4d7feebfbd2f4ed84f95912be12225749ddf6e76 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Wed, 15 Jul 2026 17:37:34 -0400 Subject: [PATCH 147/250] Test that VM clear registers VM buffers The WAL summarizer only tracks registered buffers, so unregistered VM clears are ommitted from incremental backups, corrupting the restored visibility map. Test those cases are now fixed. Author: Melanie Plageman Reviewed-by: Andrey Borodin Discussion: https://postgr.es/m/oqcsevg35xjan2327x5kdfth6q4fgeqboxfo3v3imeyih2uiny%406sez5dzxl6nt Backpatch-through: 17 --- src/bin/pg_combinebackup/Makefile | 2 + src/bin/pg_combinebackup/meson.build | 1 + .../pg_combinebackup/t/012_vm_consistency.pl | 258 ++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 src/bin/pg_combinebackup/t/012_vm_consistency.pl diff --git a/src/bin/pg_combinebackup/Makefile b/src/bin/pg_combinebackup/Makefile index 33a1f4483bf..bdba0353412 100644 --- a/src/bin/pg_combinebackup/Makefile +++ b/src/bin/pg_combinebackup/Makefile @@ -12,6 +12,8 @@ PGFILEDESC = "pg_combinebackup - combine incremental backups" PGAPPICON=win32 +EXTRA_INSTALL=contrib/pg_visibility + subdir = src/bin/pg_combinebackup top_builddir = ../../.. include $(top_builddir)/src/Makefile.global diff --git a/src/bin/pg_combinebackup/meson.build b/src/bin/pg_combinebackup/meson.build index bbc4c5735ba..757b78e2fe8 100644 --- a/src/bin/pg_combinebackup/meson.build +++ b/src/bin/pg_combinebackup/meson.build @@ -39,6 +39,7 @@ tests += { 't/009_no_full_file.pl', 't/010_hardlink.pl', 't/011_ib_truncation.pl', + 't/012_vm_consistency.pl', ], } } diff --git a/src/bin/pg_combinebackup/t/012_vm_consistency.pl b/src/bin/pg_combinebackup/t/012_vm_consistency.pl new file mode 100644 index 00000000000..6bf47ebec37 --- /dev/null +++ b/src/bin/pg_combinebackup/t/012_vm_consistency.pl @@ -0,0 +1,258 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group +# +# Test that heap operations clearing visibility map bits (INSERT, UPDATE, +# DELETE, SELECT FOR UPDATE, COPY) correctly register visibility map buffers, +# since incremental backups rely on the WAL summarizer, which only tracks +# registered buffers. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $tempdir = PostgreSQL::Test::Utils::tempdir_short(); +my $mode = $ENV{PG_TEST_PG_COMBINEBACKUP_MODE} || '--copy'; + +# Set up primary with WAL summarization enabled. +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(allows_streaming => 1); +$primary->append_conf('postgresql.conf', <start; + +$primary->safe_psql('postgres', q{CREATE EXTENSION pg_visibility}); + +my @tests = ( + { + label => 'INSERT', + table => 'vm_insert_test', + setup => q{CREATE TABLE vm_insert_test (id int); + INSERT INTO vm_insert_test DEFAULT VALUES;}, + modify => q{INSERT INTO vm_insert_test VALUES (1)}, + visible_op => '<', + frozen_op => '<', + }, + { + label => 'DELETE', + table => 'vm_delete_test', + setup => q{CREATE TABLE vm_delete_test (id int); + INSERT INTO vm_delete_test VALUES (1), (2);}, + modify => q{DELETE FROM vm_delete_test WHERE id = 1}, + visible_op => '<', + frozen_op => '<', + }, + { + label => 'UPDATE', + table => 'vm_update_test', + # Include both same-page and cross-page updates. val is stored PLAIN + # so the large update stays inline. + setup => q{CREATE TABLE vm_update_test (id INT, val TEXT); + ALTER TABLE vm_update_test ALTER COLUMN val SET STORAGE PLAIN; + INSERT INTO vm_update_test VALUES (1, 'same page'), (2, 'cross page'); + INSERT INTO vm_update_test SELECT i, repeat('a', 200) + FROM generate_series(3, 70) i;}, + modify => q{UPDATE vm_update_test SET id = 0 WHERE id = 1; + UPDATE vm_update_test SET val = repeat('b', 4000) WHERE id = 2;}, + # Confirm the small update stays on the same heap page while the large + # update relocates the tuple to a different heap page. + ctid_checks => [ + { + before => 'id = 1', + after => 'id = 0', + same => 1, + desc => 'small update stays on the same heap page', + }, + { + before => 'id = 2', + after => 'id = 2', + same => 0, + desc => 'large update moves the tuple to a different heap page', + }, + ], + visible_op => '<', + frozen_op => '<', + }, + { + label => 'LOCK', + table => 'vm_lock_test', + setup => q{CREATE TABLE vm_lock_test (id int); + INSERT INTO vm_lock_test VALUES (1), (2);}, + modify => q{SELECT * FROM vm_lock_test WHERE id = 1 FOR UPDATE}, + visible_op => '==', + frozen_op => '<', + }, + { + label => 'COPY', + table => 'vm_copy_test', + setup => q{CREATE TABLE vm_copy_test (id int); + INSERT INTO vm_copy_test DEFAULT VALUES;}, + modify => q{COPY vm_copy_test FROM PROGRAM 'echo 42'}, + visible_op => '<', + frozen_op => '<', + }, +); + +sub get_vm_summary +{ + my ($node, $table) = @_; + my $result = $node->safe_psql('postgres', + "SELECT all_visible, all_frozen FROM pg_visibility_map_summary('$table')"); + my @vals = split(/\|/, $result); + return @vals; +} + +# Return the heap block number of the (single) row matching $where. The ctid +# is "(block,offset)"; casting it through point lets us pull out the block. +sub heap_block +{ + my ($node, $table, $where) = @_; + return $node->safe_psql('postgres', + "SELECT (ctid::text::point)[0]::int FROM $table WHERE $where"); +} + +# Confirm VACUUM (FREEZE) set VM bits before testing whether later heap +# modifications clear those bits and are captured by incremental backup. We +# could perhaps be more exact than > 0, but the coarseness attempts to avoid +# test flakes. +sub check_vacuumed_vm +{ + my ($node, $test) = @_; + my ($all_visible, $all_frozen) = get_vm_summary($node, $test->{table}); + + cmp_ok($all_visible, '>', 0, + "$test->{label} test: pages are all-visible after vacuum"); + cmp_ok($all_frozen, '>', 0, + "$test->{label} test: pages are all-frozen after vacuum"); + + return ($all_visible, $all_frozen); +} + +# Check the VM bit counts after a heap modification against the post-vacuum +# baseline. Most operations clear both bits; tuple locking clears all-frozen +# without clearing all-visible. +sub check_modified_vm +{ + my ($node, $test) = @_; + my ($post_visible, $post_frozen) = get_vm_summary($node, $test->{table}); + + cmp_ok($post_visible, $test->{visible_op}, $test->{pre_visible}, + "$test->{label} test: all-visible state after modification"); + cmp_ok($post_frozen, $test->{frozen_op}, $test->{pre_frozen}, + "$test->{label} test: all-frozen state after modification"); + + return ($post_visible, $post_frozen); +} + +# Verify the combined backup restored VM state exactly as it exists on the +# primary, and ask pg_visibility to check that visible tuples are consistent. +sub validate_restored_vm +{ + my ($restored, $test) = @_; + + my ($primary_visible, $primary_frozen) = + get_vm_summary($primary, $test->{table}); + my ($restored_visible, $restored_frozen) = + get_vm_summary($restored, $test->{table}); + + is($restored_visible, $primary_visible, + "$test->{label} test: restored all_visible count matches primary"); + is($restored_frozen, $primary_frozen, + "$test->{label} test: restored all_frozen count matches primary"); + + my $corrupt_tids = $restored->safe_psql('postgres', + "SELECT count(*) FROM pg_check_visible('$test->{table}')"); + is($corrupt_tids, '0', + "$test->{label} test: no VM corruption detected by pg_check_visible"); +} + +# Create and populate the tables, then vacuum freeze them to set the VM bits +foreach my $test (@tests) +{ + $primary->safe_psql('postgres', $test->{setup}); + $primary->safe_psql('postgres', "VACUUM (FREEZE) $test->{table}"); + ($test->{pre_visible}, $test->{pre_frozen}) = + check_vacuumed_vm($primary, $test); +} + +# Take a full backup +my $full_name = 'full'; +my $full_path = $primary->backup_dir . "/$full_name"; +$primary->command_ok( + [ + 'pg_basebackup', '--no-sync', + '--pgdata' => $full_path, + '--checkpoint' => 'fast', + ], + 'full backup'); + +# Modify the tables and check that the VM bits are as expected for that test +# after the specified modification. +foreach my $test (@tests) +{ + # Record the heap block of any rows whose same-/cross-page movement we + # want to verify, before the modification relocates them. + foreach my $check (@{ $test->{ctid_checks} // [] }) + { + $check->{before_blk} = + heap_block($primary, $test->{table}, $check->{before}); + } + + $primary->safe_psql('postgres', $test->{modify}); + ($test->{post_visible}, $test->{post_frozen}) = + check_modified_vm($primary, $test); + + # Confirm each checked row did (or did not) move to a different heap page. + foreach my $check (@{ $test->{ctid_checks} // [] }) + { + my $after_blk = heap_block($primary, $test->{table}, $check->{after}); + if ($check->{same}) + { + is($after_blk, $check->{before_blk}, + "$test->{label} test: $check->{desc}"); + } + else + { + isnt($after_blk, $check->{before_blk}, + "$test->{label} test: $check->{desc}"); + } + } +} + +# Take an incremental backup. This will have the changes made in the +# modification step. +my $incr_name = 'incr'; +my $incr_path = $primary->backup_dir . "/$incr_name"; +$primary->command_ok( + [ + 'pg_basebackup', '--no-sync', + '--pgdata' => $incr_path, + '--checkpoint' => 'fast', + '--incremental' => $full_path . '/backup_manifest', + ], + 'incremental backup'); + +# Start a server from a combined backup composed of the incremental and full +# backup. +my $restored = PostgreSQL::Test::Cluster->new('restored'); +$restored->init_from_backup($primary, $incr_name, + combine_with_prior => [$full_name], + combine_mode => $mode); +$restored->append_conf('postgresql.conf', <start; +$restored->safe_psql('postgres', q{CREATE EXTENSION IF NOT EXISTS pg_visibility}); + +# Confirm that the restored server's visibility map matches the original server +foreach my $test (@tests) +{ + validate_restored_vm($restored, $test); +} + +$restored->stop; +$primary->stop; + +done_testing(); From 4a4d6d1d1d997cb46b2931abb99a1a67e59cf5ca Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 16 Jul 2026 13:35:36 +0900 Subject: [PATCH 148/250] doc: Fix log_parameter_max_length docs to reference log_min_duration_statement The documentation for log_parameter_max_length said it affects messages generated by log_duration. However, log_duration alone does not log bind parameter values, so this is misleading. This commit updates the documentation to reference log_min_duration_statement, which can log bind parameters, to better reflect actual behavior. Backpatch to all supported versions. Author: Fujii Masao Reviewed-by: Surya Poondla Discussion: https://postgr.es/m/CAHGQGwGnCVMVz8-LU9F8Sh57bkQX3jMZzx7age7M0LFEz5=Fog@mail.gmail.com Backpatch-through: 14 --- doc/src/sgml/config.sgml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index c688ed05f72..5b33465cd44 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -8041,9 +8041,10 @@ log_line_prefix = '%m [%p] %q%u@%d/%a ' This setting only affects log messages printed as a result of , - , and related settings. Non-zero - values of this setting add some overhead, particularly if parameters - are sent in binary form, since then conversion to text is required. + , and related settings. + Non-zero values of this setting add some overhead, particularly + if parameters are sent in binary form, since then conversion to + text is required. From a6a2eb9f602490ca215371ba2497b07efccaace7 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Thu, 16 Jul 2026 13:38:34 +0900 Subject: [PATCH 149/250] Check CREATE_REPLICATION_SLOT response shape in libpqwalreceiver Previously, libpqrcv_create_slot() checked only that CREATE_REPLICATION_SLOT returned PGRES_TUPLES_OK before reading values from the first row. If the server unexpectedly returned an invalid result, such as zero rows, PQgetvalue() could return NULL, leading to a crash while parsing the LSN. Other replication commands, such as IDENTIFY_SYSTEM, already validate the response shape before accessing result values, but CREATE_REPLICATION_SLOT did not. Fix this by verifying that CREATE_REPLICATION_SLOT response contains exactly one row with four fields, and report a protocol violation otherwise. Backpatch to all supported versions. Bug: #19547 Reported-by: Yuelin Wang <1217816127@qq.com> Author: Kenny Chen Reviewed-by: Hayato Kuroda Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/19547-f7986f668f71e788@postgresql.org Discussion: https://postgr.es/m/CAPXstDtW2iqe+DJAOTQTX+rRziJp2UhZSo1+HRj1COAtbu+nKw@mail.gmail.com Backpatch-through: 14 --- .../replication/libpqwalreceiver/libpqwalreceiver.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c index 7fc4ebd76bf..b0a081dd881 100644 --- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c +++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c @@ -1024,6 +1024,14 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname, slotname, pchomp(PQerrorMessage(conn->streamConn))))); } + /* CREATE_REPLICATION_SLOT returns a single row with four columns */ + if (PQnfields(res) != 4 || PQntuples(res) != 1) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("invalid response from primary server"), + errdetail("Could not create replication slot \"%s\": got %d rows and %d fields, expected %d rows and %d fields.", + slotname, PQntuples(res), PQnfields(res), 1, 4))); + if (lsn) *lsn = DatumGetLSN(DirectFunctionCall1Coll(pg_lsn_in, InvalidOid, CStringGetDatum(PQgetvalue(res, 0, 1)))); From 30c0442eb308cdb9a0d1b8551758775ec4d0e75d Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Thu, 16 Jul 2026 16:05:36 +0200 Subject: [PATCH 150/250] doc: Fix link text for data checksums Commit 67846550dc6d removed the xreflabels for initdb options, which turned the sentence "The second field contains the page checksum if data checksums are enabled" into "The second field contains the page checksum if -k are enabled", as well "Only has effect if data checksums are enabled" into "Only has effect if -k are enabled". Fix by setting an explicit link text, and while there also change the link to point to the data checksum page which has more information than just the initdb option. The original report was for one instance, further inspection turned up quite a few more cases. Also redirect the link in the amcheck docs which albeit was reading right, but will be more helpful if linking to the main page on data checksums. Backpatch to v18 where the xreflabels were removed. Author: Daniel Gustafsson Reported-by: y.saburov@gmail.com Reviewed-by: Laurenz Albe Discussion: https://postgr.es/m/178350739237.73862.4549076173872335741@wrigleys.postgresql.org Backpatch-through: 18 --- doc/src/sgml/amcheck.sgml | 5 ++--- doc/src/sgml/config.sgml | 2 +- doc/src/sgml/storage.sgml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml index 0402172a5ff..747dc115f48 100644 --- a/doc/src/sgml/amcheck.sgml +++ b/doc/src/sgml/amcheck.sgml @@ -421,9 +421,8 @@ SET client_min_messages = DEBUG1; amcheck can be effective at detecting various types of - failure modes that data - checksums will fail to catch. These include: + failure modes that data checksums will fail + to catch. These include: diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 5b33465cd44..11fcbdee046 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -12659,7 +12659,7 @@ LOG: CleanUpLock: deleting: lock(0xb7acd844) id(24688,24696,0,0,0,1) - Only has effect if are enabled. + Only has effect if data checksums are enabled. Detection of a checksum failure during a read normally causes diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml index 61250799ec0..245f18c65c9 100644 --- a/doc/src/sgml/storage.sgml +++ b/doc/src/sgml/storage.sgml @@ -793,7 +793,7 @@ data. Empty in ordinary tables. (PageHeaderData). Its format is detailed in . The first field tracks the most recent WAL entry related to this page. The second field contains - the page checksum if are + the page checksum if data checksums are enabled. Next is a 2-byte field containing flag bits. This is followed by three 2-byte integer fields (pd_lower, pd_upper, and From a00e43ac32d4f7367567791a68b2cf815f8c4d3f Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Fri, 17 Jul 2026 00:49:26 +0900 Subject: [PATCH 151/250] postgres_fdw: stabilize terminated-connection regression tests The regression test for postgres_fdw_get_connections(true) assumed that a terminated remote connection would still remain visible in the FDW connection cache long enough to be reported as closed with a nonzero remote_backend_pid. That assumption is not always valid. postgres_fdw_get_connections() reports only entries that are still present in ConnectionHash, while pgfdw_inval_callback() may immediately discard an idle cached connection (xact_depth == 0) when a relevant invalidation arrives. In CI, that can happen between terminating the remote backend and querying postgres_fdw_get_connections(true), causing the function to return no rows. Adjust the idle-connection test to accept either outcome: if the cache entry is still present, verify that it reports the expected server name, closed status, and nonzero remote backend PID; otherwise treat zero rows as a legitimate result. To preserve coverage of the terminated-backend reporting path, add a separate check inside an explicit transaction. In that case, concurrent invalidation may mark the connection invalid but cannot discard it before transaction end, so postgres_fdw_get_connections(true) should still report the terminated connection as in-use, closed, and associated with a nonzero remote backend PID. Backpatch to v18, where the affected postgres_fdw_get_connections(true) test was introduced. Reported-by: Robert Haas Author: Fujii Masao Reviewed-by: Robert Haas Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/CA+Tgmoax3cHXHsm9OidN4F-xiu16y8q2W8T5dTNFic1Zoo2cOw@mail.gmail.com Backpatch-through: 18 --- .../postgres_fdw/expected/postgres_fdw.out | 60 ++++++++++++++++--- contrib/postgres_fdw/sql/postgres_fdw.sql | 40 +++++++++++-- 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 3cc3c6dc3c7..a65f81d6eb6 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -12758,23 +12758,67 @@ SELECT server_name, loopback | f | t (1 row) --- After terminating the remote backend, since the connection is closed, --- "closed" should be TRUE, or NULL if the connection status check --- is not available. Despite the termination, remote_backend_pid should --- still show the non-zero PID of the terminated remote backend. +-- After terminating the remote backend, if the connection entry is still in +-- the cache, "closed" should be TRUE, or NULL if the connection status check +-- is not available, and remote_backend_pid should still show the non-zero PID +-- of the terminated remote backend. Concurrent invalidation can remove the +-- idle cached connection before the next statement, in which case +-- postgres_fdw_get_connections(true) can legitimately return no rows. DO $$ BEGIN PERFORM pg_terminate_backend(pid, 180000) FROM pg_stat_activity WHERE application_name = 'fdw_conn_check'; END $$; -SELECT server_name, +WITH terminated_conn AS ( + SELECT server_name, + CASE WHEN closed IS NOT false THEN true ELSE false END AS closed, + remote_backend_pid <> 0 AS remote_backend_pid + FROM postgres_fdw_get_connections(true) +) +SELECT CASE + WHEN count(*) = 0 THEN true + WHEN count(*) = 1 THEN bool_and(server_name = 'loopback' + AND closed + AND remote_backend_pid) + ELSE false +END AS ok +FROM terminated_conn; + ok +---- + t +(1 row) + +-- In an explicit transaction, concurrent invalidation may mark the +-- connection invalid but cannot discard it before transaction end, so the +-- terminated connection should remain visible in the cache. +SELECT 1 FROM postgres_fdw_disconnect_all(); + ?column? +---------- + 1 +(1 row) + +SET client_min_messages = 'ERROR'; +BEGIN; +SELECT 1 FROM ft1 LIMIT 1; + ?column? +---------- + 1 +(1 row) + +DO $$ BEGIN +PERFORM pg_terminate_backend(pid, 180000) FROM pg_stat_activity + WHERE application_name = 'fdw_conn_check'; +END $$; +SELECT server_name, used_in_xact, CASE WHEN closed IS NOT false THEN true ELSE false END AS closed, remote_backend_pid <> 0 AS remote_backend_pid FROM postgres_fdw_get_connections(true); - server_name | closed | remote_backend_pid --------------+--------+-------------------- - loopback | t | t + server_name | used_in_xact | closed | remote_backend_pid +-------------+--------------+--------+-------------------- + loopback | t | t | t (1 row) +ABORT; +RESET client_min_messages; -- Clean up \set VERBOSITY default RESET debug_discard_caches; diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 22e24bb024f..2f7b3399198 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -4451,18 +4451,48 @@ SELECT server_name, WHERE application_name = 'fdw_conn_check') AS remote_backend_pid FROM postgres_fdw_get_connections(true); --- After terminating the remote backend, since the connection is closed, --- "closed" should be TRUE, or NULL if the connection status check --- is not available. Despite the termination, remote_backend_pid should --- still show the non-zero PID of the terminated remote backend. +-- After terminating the remote backend, if the connection entry is still in +-- the cache, "closed" should be TRUE, or NULL if the connection status check +-- is not available, and remote_backend_pid should still show the non-zero PID +-- of the terminated remote backend. Concurrent invalidation can remove the +-- idle cached connection before the next statement, in which case +-- postgres_fdw_get_connections(true) can legitimately return no rows. DO $$ BEGIN PERFORM pg_terminate_backend(pid, 180000) FROM pg_stat_activity WHERE application_name = 'fdw_conn_check'; END $$; -SELECT server_name, +WITH terminated_conn AS ( + SELECT server_name, + CASE WHEN closed IS NOT false THEN true ELSE false END AS closed, + remote_backend_pid <> 0 AS remote_backend_pid + FROM postgres_fdw_get_connections(true) +) +SELECT CASE + WHEN count(*) = 0 THEN true + WHEN count(*) = 1 THEN bool_and(server_name = 'loopback' + AND closed + AND remote_backend_pid) + ELSE false +END AS ok +FROM terminated_conn; + +-- In an explicit transaction, concurrent invalidation may mark the +-- connection invalid but cannot discard it before transaction end, so the +-- terminated connection should remain visible in the cache. +SELECT 1 FROM postgres_fdw_disconnect_all(); +SET client_min_messages = 'ERROR'; +BEGIN; +SELECT 1 FROM ft1 LIMIT 1; +DO $$ BEGIN +PERFORM pg_terminate_backend(pid, 180000) FROM pg_stat_activity + WHERE application_name = 'fdw_conn_check'; +END $$; +SELECT server_name, used_in_xact, CASE WHEN closed IS NOT false THEN true ELSE false END AS closed, remote_backend_pid <> 0 AS remote_backend_pid FROM postgres_fdw_get_connections(true); +ABORT; +RESET client_min_messages; -- Clean up \set VERBOSITY default From c31b0fca059cf081679be1689b59b21912b0d29e Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Thu, 16 Jul 2026 11:50:13 -0700 Subject: [PATCH 152/250] Reject infinite and out-of-range interval shifts in uuidv7(). uuidv7(interval) shifts the current time by the given interval before encoding it into the 48-bit Unix-millisecond timestamp field of the generated UUID. Two cases were mishandled: An infinite interval ('infinity' or '-infinity') produced an infinite timestamp, which overflowed during the conversion to Unix-epoch microseconds and yielded a garbage UUID. Reject infinite intervals up front, before any timestamp arithmetic. A shift that moved the timestamp outside the range representable by the 48-bit field was silently accepted. Timestamps before the Unix epoch wrapped when cast to unsigned, and timestamps beyond approximately year 10889 overflowed the field; both produced UUIDs with bogus timestamps that break sort ordering. Reject any shifted timestamp outside the supported range. Also document that infinite intervals and out-of-range shifts are rejected. Although raising a new error changes behavior in a stable branch, this is back-patched to 18 (where uuidv7(interval) was introduced) because the previous behavior can silently corrupt data. Failing loudly is far safer than silently accepting the wraparound; otherwise users may not discover that their UUIDv7 values are no longer sortable until years later, when recovery is painful. It also matches how PostgreSQL already handles timestamp + interval overflow, which raises an error. The change only affects applications passing an interval large enough to push the result outside the representable range. Backpatch to 18, where uuidv7(interval) was introduced. Reported-by: Christophe Pettus Author: Baji Shaik Reviewed-by: Masahiko Sawada Reviewed-by: Zsolt Parragi Reviewed-by: Tristan Partin Reviewed-by: Kyotaro Horiguchi Discussion: https://www.postgresql.org/message-id/799A70FA-6E5C-4118-99EB-2FBBE1CBAC54@thebuild.com Backpatch-through: 18 --- doc/src/sgml/func.sgml | 6 ++++ src/backend/utils/adt/uuid.c | 45 ++++++++++++++++++++++++++---- src/test/regress/expected/uuid.out | 22 +++++++++++++++ src/test/regress/sql/uuid.sql | 13 +++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 3af0a615c04..b57f161f53c 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -14465,6 +14465,12 @@ CREATE TYPE rainbow AS ENUM ('red', 'orange', 'yellow', 'green', 'blue', 'purple sub-millisecond timestamp + random. The optional parameter shift will shift the computed timestamp by the given interval. + Infinite interval values are not accepted. + The shifted timestamp must fall within the range supported by + UUID version 7's 48-bit millisecond timestamp field: from + 1970-01-01 00:00:00 UTC to approximately year 10889. + An error is raised if the resulting timestamp is outside this + range. uuidv7() diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c index a746388b073..c910836da06 100644 --- a/src/backend/utils/adt/uuid.c +++ b/src/backend/utils/adt/uuid.c @@ -33,6 +33,23 @@ #define NS_PER_US INT64CONST(1000) #define US_PER_MS INT64CONST(1000) +/* + * The offset between the PostgreSQL epoch (2000-01-01) and the Unix epoch + * (1970-01-01) in microseconds. Subtract this from a Unix-epoch microseconds + * to get a TimestampTz. + */ +#define PG_UNIX_EPOCH_OFFSET_US \ + ((int64) (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC) + +/* + * Valid timestamp range for UUID version 7, expressed in PostgreSQL-epoch + * microseconds. UUIDv7 uses a 48-bit unsigned millisecond field relative + * to the Unix epoch, so the representable window is [1970-01-01, ~10889]. + */ +#define UUIDV7_MIN_TIMESTAMP (-PG_UNIX_EPOCH_OFFSET_US) +#define UUIDV7_MAX_TIMESTAMP \ + (((INT64CONST(1) << 48) - 1) * US_PER_MS - PG_UNIX_EPOCH_OFFSET_US) + /* * UUID version 7 uses 12 bits in "rand_a" to store 1/4096 (or 2^12) fractions of * sub-millisecond. While most Unix-like platforms provide nanosecond-precision @@ -676,6 +693,13 @@ uuidv7_interval(PG_FUNCTION_ARGS) int64 ns = get_real_time_ns_ascending(); int64 us; + /* Reject infinite intervals before any arithmetic */ + if (INTERVAL_NOT_FINITE(shift)) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("interval out of range for UUID version 7"), + errdetail("UUID version 7 does not support infinite intervals."))); + /* * Shift the current timestamp by the given interval. To calculate time * shift correctly, we convert the UNIX epoch to TimestampTz and use @@ -683,16 +707,26 @@ uuidv7_interval(PG_FUNCTION_ARGS) * precision. */ - ts = (TimestampTz) (ns / NS_PER_US) - - (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC; + ts = (TimestampTz) (ns / NS_PER_US) - PG_UNIX_EPOCH_OFFSET_US; /* Compute time shift */ ts = DatumGetTimestampTz(DirectFunctionCall2(timestamptz_pl_interval, TimestampTzGetDatum(ts), IntervalPGetDatum(shift))); - /* Convert a TimestampTz value back to an UNIX epoch timestamp */ - us = ts + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC; + /* + * Reject timestamps outside the range representable by UUID version 7's + * 48-bit millisecond field. We compare in PostgreSQL-epoch units so that + * the subsequent conversion to Unix-epoch microseconds cannot overflow. + */ + if (ts < UUIDV7_MIN_TIMESTAMP || ts > UUIDV7_MAX_TIMESTAMP) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("timestamp out of range for UUID version 7"), + errdetail("UUID version 7 supports timestamps from 1970-01-01 to approximately year 10889."))); + + /* Convert the TimestampTz value to a Unix-epoch timestamp in usec */ + us = ts + PG_UNIX_EPOCH_OFFSET_US; /* Generate an UUIDv7 */ uuid = generate_uuidv7(us / US_PER_MS, (us % US_PER_MS) * NS_PER_US + ns % NS_PER_US); @@ -752,8 +786,7 @@ uuid_extract_timestamp(PG_FUNCTION_ARGS) + (((uint64) uuid->data[0]) << 40); /* convert ms to us, then adjust */ - ts = (TimestampTz) (tms * US_PER_MS) - - (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC; + ts = (TimestampTz) (tms * US_PER_MS) - PG_UNIX_EPOCH_OFFSET_US; PG_RETURN_TIMESTAMPTZ(ts); } diff --git a/src/test/regress/expected/uuid.out b/src/test/regress/expected/uuid.out index 95392003b86..5212eb992bf 100644 --- a/src/test/regress/expected/uuid.out +++ b/src/test/regress/expected/uuid.out @@ -248,6 +248,28 @@ SELECT y, ts, prev_ts FROM uuidts WHERE ts < prev_ts; ---+----+--------- (0 rows) +-- uuidv7: infinite intervals are rejected +SELECT uuidv7('infinity'::interval); +ERROR: interval out of range for UUID version 7 +DETAIL: UUID version 7 does not support infinite intervals. +SELECT uuidv7('-infinity'::interval); +ERROR: interval out of range for UUID version 7 +DETAIL: UUID version 7 does not support infinite intervals. +-- uuidv7: timestamps before Unix epoch are rejected +SELECT uuidv7('-1000 years'::interval); +ERROR: timestamp out of range for UUID version 7 +DETAIL: UUID version 7 supports timestamps from 1970-01-01 to approximately year 10889. +-- uuidv7: timestamps beyond 48-bit ms field (~year 10889) are rejected +SELECT uuidv7('9000 years'::interval); +ERROR: timestamp out of range for UUID version 7 +DETAIL: UUID version 7 supports timestamps from 1970-01-01 to approximately year 10889. +-- uuidv7: a large but in-range forward shift is accepted +SELECT uuid_extract_timestamp(uuidv7('1000 years'::interval)) > now() + '999 years'::interval; + ?column? +---------- + t +(1 row) + -- extract functions -- version SELECT uuid_extract_version('11111111-1111-5111-8111-111111111111'); -- 5 diff --git a/src/test/regress/sql/uuid.sql b/src/test/regress/sql/uuid.sql index 465153a0341..e92dd2f2ead 100644 --- a/src/test/regress/sql/uuid.sql +++ b/src/test/regress/sql/uuid.sql @@ -131,6 +131,19 @@ WITH uuidts AS ( ) SELECT y, ts, prev_ts FROM uuidts WHERE ts < prev_ts; +-- uuidv7: infinite intervals are rejected +SELECT uuidv7('infinity'::interval); +SELECT uuidv7('-infinity'::interval); + +-- uuidv7: timestamps before Unix epoch are rejected +SELECT uuidv7('-1000 years'::interval); + +-- uuidv7: timestamps beyond 48-bit ms field (~year 10889) are rejected +SELECT uuidv7('9000 years'::interval); + +-- uuidv7: a large but in-range forward shift is accepted +SELECT uuid_extract_timestamp(uuidv7('1000 years'::interval)) > now() + '999 years'::interval; + -- extract functions -- version From 8af1f527842170a257ae0684dd02907135d8d2e5 Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Thu, 16 Jul 2026 18:55:33 -0400 Subject: [PATCH 153/250] Fix wrong variable offset sanity check. Commit c7aeb775 rewrote the HOT-chain offset sanity checks in three places, but in heap_get_root_tuples it accidentally tested offnum -- the outer loop variable, which is already bounded by the loop condition -- instead of nextoffnum, the offset actually passed to PageGetItemId. The pre-c7aeb775 check tested nextoffnum. With the check ineffective, a stale t_ctid could make PageGetItemId read past the end of the line pointer array (which is data corruption that we expect to be able to catch here). Author: Peter Geoghegan Reported-by: Konstantin Knizhnik Discussion: https://postgr.es/m/87c7d8a4-3a82-4334-bee6-e8c2ad3f3293@garret.ru Backpatch-through: 15 --- src/backend/access/heap/pruneheap.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index a8025889be0..a36c6b842e8 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -1848,14 +1848,14 @@ heap_get_root_tuples(Page page, OffsetNumber *root_offsets) for (;;) { /* Sanity check (pure paranoia) */ - if (offnum < FirstOffsetNumber) + if (nextoffnum < FirstOffsetNumber) break; /* * An offset past the end of page's line pointer array is possible * when the array was truncated */ - if (offnum > maxoff) + if (nextoffnum > maxoff) break; lp = PageGetItemId(page, nextoffnum); From aa572d521a1116b2c8902b98f44baf402a3e5246 Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Fri, 17 Jul 2026 09:33:16 +0530 Subject: [PATCH 154/250] Doc: Clarify DROP SUBSCRIPTION behavior after SET (slot_name = NONE). The previous text claimed that once the slot is disassociated with ALTER SUBSCRIPTION ... SET (slot_name = NONE), DROP SUBSCRIPTION "will no longer attempt any actions on a remote host". That is inaccurate: DROP SUBSCRIPTION may still connect to the publisher to drop internally-created table synchronization slots when some table synchronization is left unfinished. Reword to describe this, and note that if the publisher is unreachable those slots (and the main slot, if it still exists) must be dropped manually to avoid indefinitely reserving WAL. Reported-by: Jeff Davis Author: Amit Kapila Backpatch-through: 14 Discussion: https://postgr.es/m/CAA4eK1+tyYSpPxMBy1974kjivuGeR7YY=yopwRGrK3+vCTysdg@mail.gmail.com Discussion: https://postgr.es/m/D908370F-2695-4231-851D-17179A6A6F2A@gmail.com --- doc/src/sgml/ref/drop_subscription.sgml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/src/sgml/ref/drop_subscription.sgml b/doc/src/sgml/ref/drop_subscription.sgml index 6e84bb0a256..209416756e5 100644 --- a/doc/src/sgml/ref/drop_subscription.sgml +++ b/doc/src/sgml/ref/drop_subscription.sgml @@ -102,12 +102,14 @@ DROP SUBSCRIPTION [ IF EXISTS ] name ALTER SUBSCRIPTION ... SET (slot_name = NONE). - After that, DROP SUBSCRIPTION will no longer attempt any - actions on a remote host. Note that if the remote replication slot still - exists, it (and any related table synchronization slots) should then be - dropped manually; otherwise it/they will continue to - reserve WAL and might eventually cause the disk to fill up. See - also . + After that, DROP SUBSCRIPTION will not attempt to drop + the subscription's own replication slot. It may still connect to the publisher + to drop internally-created table synchronization slots if some table + synchronization is left unfinished; if the publisher is unreachable, those + slots (and the main slot, if it still exists) must be dropped manually. Otherwise + it/they will continue to reserve WAL and might eventually cause the disk to + fill up. See also + . From 125893ce83b8218db8446f5f6f416cdbf9e80f7b Mon Sep 17 00:00:00 2001 From: Andres Freund Date: Fri, 17 Jul 2026 11:23:18 -0400 Subject: [PATCH 155/250] meson: Fix ccache issues when using precompiled headers with gcc Unfortunately the combination of gcc, precompiled headers, ccache and meson currently is not safe without further options. The dependencies emitted by gcc are insufficient to trigger rebuilds when headers "below" the precompiled headers are changed. Whether that's a ccache, gcc or meson bug is debatable. Luckily gcc's -fpch-deps option fixes the issue. This problem occasionally leads to build failures, e.g. if only c.h, postgres.h or pg_config_manual.h change. That's e.g. the case when creating a new major version branch. Reviewed-by: Nazir Bilal Yavuz Reviewed-by: Jelte Fennema-Nio Discussion: https://postgr.es/m/CAN55FZ0tqR6Xz%3DiVFLc1BBoLOEHU775ARhcGYwggHA3XLA%3DoQg%40mail.gmail.com Discussion: https://postgr.es/m/CA+hUKG+s7Yvt0PUnSQUEjCjysV-7-51n9B1h468Le3VJi0x4ZQ@mail.gmail.com Discussion: https://postgr.es/m/phsrssp75npoyalqsolcd7fmnmlbzbmquc2p7w7mqjlw7432jk@bzskz3luyjvb Discussion: https://github.com/ccache/ccache/issues/1686 Backpatch-through: 16, where meson support was added --- meson.build | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/meson.build b/meson.build index 2c94236a072..3449ae9fab2 100644 --- a/meson.build +++ b/meson.build @@ -2096,6 +2096,12 @@ common_functional_flags = [ # Disable optimizations that assume no overflow; needed for gcc 4.3+ '-fwrapv', '-fexcess-precision=standard', + # Without -fpch-deps gcc emits dependencies that are insufficient for ccache + # to trigger a rebuild when the precompiled header changes. We could make + # this depend on using gcc and precompiled headers being enabled, but that's + # probably not worth it. See also + # https://github.com/ccache/ccache/issues/1686 + '-fpch-deps', ] cflags += cc.get_supported_arguments(common_functional_flags) From 959b7fa2cd603ec353aaef715993d0f0816febd2 Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Fri, 17 Jul 2026 15:53:05 -0400 Subject: [PATCH 156/250] Fix GiST index-only scan column alignment issue. An index-only scan filled its result slot from the HeapTuple an index AM returns in scan->xs_hitup by deforming it with the virtual slot's own tuple descriptor (during GiST and SP-GiST index-only scans). But index AMs form that heap tuple using their own descriptor, scan->xs_hitupdesc. The AM's descriptor may disagree with the IoS virtual slot's descriptor about each column's precise alignment, leading to "can't happen" errors in certain rare edge cases. Hard crashes were possible but much less likely. To fix, deform the tuple with the descriptor it was formed with. This is simpler, and makes xs_hitup handling (used by GiST and SP-GiST) uniform with the nearby existing xs_itup handling (used by nbtree). In practice this issue was very unlikely to be hit (it was found during testing of a patch that will change the table AM API used during index scans). The only currently affected core opclass is GiST's range_ops. It was only possible for the datum to be accessed at an incorrectly aligned offset when reading the second or subsequent column from a multicolumn GiST index. This couldn't happen in the common case where the datum used an unaligned short varlena header. Moreover, an earlier column had to leave the range datum at an offset where the two alignments actually disagree (e.g., an odd-length varlena datum). Author: Peter Geoghegan Reviewed-by: Tomas Vondra Discussion: https://postgr.es/m/CAH2-WzkGXa2SKnebdW29RT1hCcQBo_p03v3iqif2u9bjzLB-aQ@mail.gmail.com Backpatch-through: 14 --- src/backend/executor/nodeIndexonlyscan.c | 113 ++++++++++++----------- src/test/regress/expected/gist.out | 36 ++++++++ src/test/regress/sql/gist.sql | 29 ++++++ 3 files changed, 124 insertions(+), 54 deletions(-) diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c index a6a00d594c0..867a8e40e78 100644 --- a/src/backend/executor/nodeIndexonlyscan.c +++ b/src/backend/executor/nodeIndexonlyscan.c @@ -31,6 +31,7 @@ #include "postgres.h" #include "access/genam.h" +#include "access/htup_details.h" #include "access/relscan.h" #include "access/tableam.h" #include "access/tupdesc.h" @@ -48,7 +49,7 @@ static TupleTableSlot *IndexOnlyNext(IndexOnlyScanState *node); static void StoreIndexTuple(IndexOnlyScanState *node, TupleTableSlot *slot, - IndexTuple itup, TupleDesc itupdesc); + IndexScanDesc scandesc); /* ---------------------------------------------------------------- @@ -190,27 +191,8 @@ IndexOnlyNext(IndexOnlyScanState *node) tuple_from_heap = true; } - /* - * Fill the scan tuple slot with data from the index. This might be - * provided in either HeapTuple or IndexTuple format. Conceivably an - * index AM might fill both fields, in which case we prefer the heap - * format, since it's probably a bit cheaper to fill a slot from. - */ - if (scandesc->xs_hitup) - { - /* - * We don't take the trouble to verify that the provided tuple has - * exactly the slot's format, but it seems worth doing a quick - * check on the number of fields. - */ - Assert(slot->tts_tupleDescriptor->natts == - scandesc->xs_hitupdesc->natts); - ExecForceStoreHeapTuple(scandesc->xs_hitup, slot, false); - } - else if (scandesc->xs_itup) - StoreIndexTuple(node, slot, scandesc->xs_itup, scandesc->xs_itupdesc); - else - elog(ERROR, "no data returned for index-only scan"); + /* Fill the scan tuple slot with data from the index */ + StoreIndexTuple(node, slot, scandesc); /* * If the index was lossy, we have to recheck the index quals. @@ -260,56 +242,79 @@ IndexOnlyNext(IndexOnlyScanState *node) /* * StoreIndexTuple - * Fill the slot with data from the index tuple. + * Fill the slot with the data the index AM returned. + * + * The data might be provided in either HeapTuple (xs_hitup) or IndexTuple + * (xs_itup) format. Conceivably an index AM might fill both fields, in which + * case we prefer the heap format, since it's probably a bit cheaper to fill a + * slot from. * * At some point this might be generally-useful functionality, but * right now we don't need it elsewhere. */ static void StoreIndexTuple(IndexOnlyScanState *node, TupleTableSlot *slot, - IndexTuple itup, TupleDesc itupdesc) + IndexScanDesc scandesc) { - /* - * Note: we must use the tupdesc supplied by the AM in index_deform_tuple, - * not the slot's tupdesc, in case the latter has different datatypes - * (this happens for btree name_ops in particular). They'd better have - * the same number of columns though, as well as being datatype-compatible - * which is something we can't so easily check. - */ - Assert(slot->tts_tupleDescriptor->natts == itupdesc->natts); - ExecClearTuple(slot); - index_deform_tuple(itup, itupdesc, slot->tts_values, slot->tts_isnull); /* - * Copy all name columns stored as cstrings back into a NAMEDATALEN byte - * sized allocation. We mark this branch as unlikely as generally "name" - * is used only for the system catalogs and this would have to be a user - * query running on those or some other user table with an index on a name - * column. + * We must deform the tuple using the tupdesc the index AM formed it with + * (xs_hitupdesc or xs_itupdesc), not the slot's tupdesc. The datums + * returned by the index AM must be binary compatible, but the descriptors + * may align each column differently in certain rare cases. (Actually, + * btree's "name" opclass stores cstring tuples that _aren't_ even binary + * compatible, in the strictest sense. We directly handle that here.) */ - if (unlikely(node->ioss_NameCStringAttNums != NULL)) + if (scandesc->xs_hitup) { - int attcount = node->ioss_NameCStringCount; + Assert(slot->tts_tupleDescriptor->natts == scandesc->xs_hitupdesc->natts); - for (int idx = 0; idx < attcount; idx++) - { - int attnum = node->ioss_NameCStringAttNums[idx]; - Name name; + heap_deform_tuple(scandesc->xs_hitup, scandesc->xs_hitupdesc, + slot->tts_values, slot->tts_isnull); + } + else if (scandesc->xs_itup) + { + Assert(slot->tts_tupleDescriptor->natts == scandesc->xs_itupdesc->natts); - /* skip null Datums */ - if (slot->tts_isnull[attnum]) - continue; + index_deform_tuple(scandesc->xs_itup, scandesc->xs_itupdesc, + slot->tts_values, slot->tts_isnull); - /* allocate the NAMEDATALEN and copy the datum into that memory */ - name = (Name) MemoryContextAlloc(node->ss.ps.ps_ExprContext->ecxt_per_tuple_memory, - NAMEDATALEN); + /* + * Copy all name columns stored as cstrings back into a NAMEDATALEN + * byte sized allocation. We mark this branch as unlikely as + * generally "name" is used only for the system catalogs and this + * would have to be a user query running on those or some other user + * table with an index on a name column. + */ + if (unlikely(node->ioss_NameCStringAttNums != NULL)) + { + int attcount = node->ioss_NameCStringCount; - /* use namestrcpy to zero-pad all trailing bytes */ - namestrcpy(name, DatumGetCString(slot->tts_values[attnum])); - slot->tts_values[attnum] = NameGetDatum(name); + for (int idx = 0; idx < attcount; idx++) + { + int attnum = node->ioss_NameCStringAttNums[idx]; + Name name; + + /* skip null Datums */ + if (slot->tts_isnull[attnum]) + continue; + + /* + * allocate the NAMEDATALEN and copy the datum into that + * memory + */ + name = (Name) MemoryContextAlloc(node->ss.ps.ps_ExprContext->ecxt_per_tuple_memory, + NAMEDATALEN); + + /* use namestrcpy to zero-pad all trailing bytes */ + namestrcpy(name, DatumGetCString(slot->tts_values[attnum])); + slot->tts_values[attnum] = NameGetDatum(name); + } } } + else + elog(ERROR, "no data returned for index-only scan"); ExecStoreVirtualTuple(slot); } diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out index c75bbb23b6e..ae5b522b3c6 100644 --- a/src/test/regress/expected/gist.out +++ b/src/test/regress/expected/gist.out @@ -387,6 +387,42 @@ select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; ERROR: lossy distance functions are not supported in index-only scans +-- Test that an index-only scan deforms the tuple it reconstructs with the +-- descriptor the AM formed it with, not the scan slot's descriptor. +create temp table gist_ios_tupdesc (a inet, r numrange); +-- range_ops forms its tuples using the opclass input type, the polymorphic +-- anyrange (alignment 'd'), while the scan slot uses the actual range type +-- numrange (alignment 'i'). A buggy implementation will incorrectly access +-- the r/numrange column at the wrong offset. +-- +-- The range bounds are made long so the value needs a four-byte varlena +-- header; shorter values get a one-byte header and are stored without +-- alignment padding, which would mask the problem. +insert into gist_ios_tupdesc +values ( + '::1', -- shifts "r" datum value to differing offset + numrange(repeat('7', 200)::numeric, repeat('8', 200)::numeric)); +create index on gist_ios_tupdesc using gist (a inet_ops, r); +vacuum analyze gist_ios_tupdesc; +explain (costs off) +select lower(r) = repeat('7', 200)::numeric as lower_ok, + upper(r) = repeat('8', 200)::numeric as upper_ok + from gist_ios_tupdesc where r && numrange(null, null); + QUERY PLAN +-------------------------------------------------------------------- + Index Only Scan using gist_ios_tupdesc_a_r_idx on gist_ios_tupdesc + Index Cond: (r && '(,)'::numrange) +(2 rows) + +select lower(r) = repeat('7', 200)::numeric as lower_ok, + upper(r) = repeat('8', 200)::numeric as upper_ok + from gist_ios_tupdesc where r && numrange(null, null); + lower_ok | upper_ok +----------+---------- + t | t +(1 row) + +drop table gist_ios_tupdesc; -- Force an index build using buffering. create index gist_tbl_box_index_forcing_buffering on gist_tbl using gist (p) with (buffering=on, fillfactor=50); diff --git a/src/test/regress/sql/gist.sql b/src/test/regress/sql/gist.sql index 6f1fc65f128..1ebb1d9ee43 100644 --- a/src/test/regress/sql/gist.sql +++ b/src/test/regress/sql/gist.sql @@ -169,6 +169,35 @@ explain (verbose, costs off) select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; +-- Test that an index-only scan deforms the tuple it reconstructs with the +-- descriptor the AM formed it with, not the scan slot's descriptor. +create temp table gist_ios_tupdesc (a inet, r numrange); + +-- range_ops forms its tuples using the opclass input type, the polymorphic +-- anyrange (alignment 'd'), while the scan slot uses the actual range type +-- numrange (alignment 'i'). A buggy implementation will incorrectly access +-- the r/numrange column at the wrong offset. +-- +-- The range bounds are made long so the value needs a four-byte varlena +-- header; shorter values get a one-byte header and are stored without +-- alignment padding, which would mask the problem. +insert into gist_ios_tupdesc +values ( + '::1', -- shifts "r" datum value to differing offset + numrange(repeat('7', 200)::numeric, repeat('8', 200)::numeric)); +create index on gist_ios_tupdesc using gist (a inet_ops, r); +vacuum analyze gist_ios_tupdesc; + +explain (costs off) +select lower(r) = repeat('7', 200)::numeric as lower_ok, + upper(r) = repeat('8', 200)::numeric as upper_ok + from gist_ios_tupdesc where r && numrange(null, null); +select lower(r) = repeat('7', 200)::numeric as lower_ok, + upper(r) = repeat('8', 200)::numeric as upper_ok + from gist_ios_tupdesc where r && numrange(null, null); + +drop table gist_ios_tupdesc; + -- Force an index build using buffering. create index gist_tbl_box_index_forcing_buffering on gist_tbl using gist (p) with (buffering=on, fillfactor=50); From 21f5e659e7587abf640f9c4aaff2238e3c3ff683 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sat, 18 Jul 2026 14:09:10 -0400 Subject: [PATCH 157/250] Fix edge case in remove_useless_result_rtes() with outer joins. find_dependent_phvs() and find_dependent_phvs_in_jointree() decide whether a PlaceHolderVar depends on the RTE_RESULT rel we're considering removing by comparing the PHV's phrels to a singleton set containing that rel's RT index, reasoning that if phrels contains any other relid bits then those define an appropriate place where we can evaluate the PHV. But since this code was originally written, we've redefined phrels to include outer-join relids, and that breaks this logic, potentially allowing us to remove an RTE_RESULT that leaves no valid place to evaluate the PHV. The planner doesn't throw an error when that happens, but it does produce an incorrect plan that will not replace the PHV's value with NULL when needed. In the known test case for this bug, the "extra" OJ relid is one that we've actually decided to remove but haven't yet cleaned out of the query's PHVs. It's not entirely clear though that that would always be the case. Let's restore this code to the way it was designed to work, by considering only base relids within the PHV's phrels. Bug: #19553 Reported-by: Viktor Leis Author: Matheus Alcantara Co-authored-by: Richard Guo Reviewed-by: Tom Lane Discussion: https://postgr.es/m/19553-4561747f93f368a7@postgresql.org Backpatch-through: 16 --- src/backend/optimizer/prep/prepjointree.c | 64 ++++++++++++++++++----- src/test/regress/expected/join.out | 27 ++++++++++ src/test/regress/sql/join.sql | 10 ++++ 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 7bf4e55c7a1..cc10c5c1ddc 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -158,13 +158,15 @@ static void reduce_outer_joins_pass2(Node *jtnode, static void report_reduced_full_join(reduce_outer_joins_pass2_state *state2, int rtindex, Relids relids); static Node *remove_useless_results_recurse(PlannerInfo *root, Node *jtnode, + Relids baserels, Node **parent_quals, Relids *dropped_outer_joins); static int get_result_relid(PlannerInfo *root, Node *jtnode); static void remove_result_refs(PlannerInfo *root, int varno, Node *newjtloc); -static bool find_dependent_phvs(PlannerInfo *root, int varno); +static bool find_dependent_phvs(PlannerInfo *root, int varno, Relids baserels); static bool find_dependent_phvs_in_jointree(PlannerInfo *root, - Node *node, int varno); + Node *node, int varno, + Relids baserels); static void substitute_phv_relids(Node *node, int varno, Relids subrelids); static void fix_append_rel_relids(PlannerInfo *root, int varno, @@ -3614,15 +3616,24 @@ report_reduced_full_join(reduce_outer_joins_pass2_state *state2, void remove_useless_result_rtes(PlannerInfo *root) { + Relids baserels; Relids dropped_outer_joins = NULL; ListCell *cell; + /* + * We'll need the set of baserels in the jointree to perform + * find_dependent_phvs() checks. + */ + baserels = get_relids_in_jointree((Node *) root->parse->jointree, + false, false); + /* Top level of jointree must always be a FromExpr */ Assert(IsA(root->parse->jointree, FromExpr)); /* Recurse ... */ root->parse->jointree = (FromExpr *) remove_useless_results_recurse(root, (Node *) root->parse->jointree, + baserels, NULL, &dropped_outer_joins); /* We should still have a FromExpr */ @@ -3683,9 +3694,12 @@ remove_useless_result_rtes(PlannerInfo *root) * the parent's quals list; otherwise, pass NULL for parent_quals. * (Note that in some cases, parent_quals points to the quals of a parent * more than one level up in the tree.) + * + * baserels is the set of base (non-join) RT indexes in the whole jointree. */ static Node * remove_useless_results_recurse(PlannerInfo *root, Node *jtnode, + Relids baserels, Node **parent_quals, Relids *dropped_outer_joins) { @@ -3716,6 +3730,7 @@ remove_useless_results_recurse(PlannerInfo *root, Node *jtnode, /* Recursively transform child, allowing it to push up quals ... */ child = remove_useless_results_recurse(root, child, + baserels, &f->quals, dropped_outer_joins); /* ... and stick it back into the tree */ @@ -3729,7 +3744,8 @@ remove_useless_results_recurse(PlannerInfo *root, Node *jtnode, */ if (list_length(f->fromlist) > 1 && (varno = get_result_relid(root, child)) != 0 && - !find_dependent_phvs_in_jointree(root, (Node *) f, varno)) + !find_dependent_phvs_in_jointree(root, (Node *) f, varno, + baserels)) { f->fromlist = foreach_delete_current(f->fromlist, cell); result_relids = bms_add_member(result_relids, varno); @@ -3798,12 +3814,14 @@ remove_useless_results_recurse(PlannerInfo *root, Node *jtnode, * quals up, or at least there's no particular reason to. */ j->larg = remove_useless_results_recurse(root, j->larg, + baserels, (j->jointype == JOIN_INNER) ? &j->quals : (j->jointype == JOIN_LEFT) ? parent_quals : NULL, dropped_outer_joins); j->rarg = remove_useless_results_recurse(root, j->rarg, + baserels, (j->jointype == JOIN_INNER || j->jointype == JOIN_LEFT) ? &j->quals : NULL, @@ -3831,7 +3849,8 @@ remove_useless_results_recurse(PlannerInfo *root, Node *jtnode, * allowed to have such refs. */ if ((varno = get_result_relid(root, j->larg)) != 0 && - !find_dependent_phvs_in_jointree(root, j->rarg, varno)) + !find_dependent_phvs_in_jointree(root, j->rarg, varno, + baserels)) { remove_result_refs(root, varno, j->rarg); if (j->quals != NULL && parent_quals == NULL) @@ -3886,7 +3905,7 @@ remove_useless_results_recurse(PlannerInfo *root, Node *jtnode, */ if ((varno = get_result_relid(root, j->rarg)) != 0 && (j->quals == NULL || - !find_dependent_phvs(root, varno))) + !find_dependent_phvs(root, varno, baserels))) { remove_result_refs(root, varno, j->larg); *dropped_outer_joins = bms_add_member(*dropped_outer_joins, @@ -4008,9 +4027,17 @@ remove_result_refs(PlannerInfo *root, int varno, Node *newjtloc) /* - * find_dependent_phvs - are there any PlaceHolderVars whose relids are + * find_dependent_phvs - are there any PlaceHolderVars whose base relids are * exactly the given varno? * + * We ignore outer-join relids present in a PHV's phrels, by intersecting + * with the caller-supplied "baserels" set. This is necessary in part + * because some of the OJ relids may be stale, that is we may have + * already decided to remove those joins in remove_useless_result_rtes + * and not yet have cleaned their relid bits out of upper PHVs. + * But in general, it's the set of baserels that identify possible places + * to evaluate a PHV, and we mustn't let that go to empty. + * * find_dependent_phvs should be used when we want to see if there are * any such PHVs anywhere in the Query. Another use-case is to see if * a subtree of the join tree contains such PHVs; but for that, we have @@ -4020,8 +4047,9 @@ remove_result_refs(PlannerInfo *root, int varno, Node *newjtloc) typedef struct { - Relids relids; - int sublevels_up; + Relids relids; /* target relid, represented as a relid set */ + Relids baserels; /* set of base (non-OJ) RT indexes in query */ + int sublevels_up; /* current nesting level */ } find_dependent_phvs_context; static bool @@ -4034,9 +4062,16 @@ find_dependent_phvs_walker(Node *node, { PlaceHolderVar *phv = (PlaceHolderVar *) node; - if (phv->phlevelsup == context->sublevels_up && - bms_equal(context->relids, phv->phrels)) - return true; + if (phv->phlevelsup == context->sublevels_up) + { + Relids phbaserels = bms_intersect(phv->phrels, + context->baserels); + bool match = bms_equal(context->relids, phbaserels); + + bms_free(phbaserels); + if (match) + return true; + } /* fall through to examine children */ } if (IsA(node, Query)) @@ -4060,7 +4095,7 @@ find_dependent_phvs_walker(Node *node, } static bool -find_dependent_phvs(PlannerInfo *root, int varno) +find_dependent_phvs(PlannerInfo *root, int varno, Relids baserels) { find_dependent_phvs_context context; @@ -4069,6 +4104,7 @@ find_dependent_phvs(PlannerInfo *root, int varno) return false; context.relids = bms_make_singleton(varno); + context.baserels = baserels; context.sublevels_up = 0; if (query_tree_walker(root->parse, find_dependent_phvs_walker, &context, 0)) @@ -4082,7 +4118,8 @@ find_dependent_phvs(PlannerInfo *root, int varno) } static bool -find_dependent_phvs_in_jointree(PlannerInfo *root, Node *node, int varno) +find_dependent_phvs_in_jointree(PlannerInfo *root, Node *node, int varno, + Relids baserels) { find_dependent_phvs_context context; Relids subrelids; @@ -4093,6 +4130,7 @@ find_dependent_phvs_in_jointree(PlannerInfo *root, Node *node, int varno) return false; context.relids = bms_make_singleton(varno); + context.baserels = baserels; context.sublevels_up = 0; /* diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 86078cbf27d..9fc12fb0cb0 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -4133,6 +4133,33 @@ select * from 1 | 2 | 2 (1 row) +-- Also, we mustn't remove an RTE_RESULT that is the only baserel where a PHV +-- can be evaluated, even when the PHV's phrels also mention an outer join. +explain (verbose, costs off) +select * from (values (1),(2)) v(x) + left join (select q from (select 7 as q from (select where false) ss1) ss2 + left join (select 8 as z) ss3 on true) ss4 on true; + QUERY PLAN +------------------------------------ + Nested Loop Left Join + Output: "*VALUES*".column1, (7) + Join Filter: false + -> Values Scan on "*VALUES*" + Output: "*VALUES*".column1 + -> Result + Output: 7 + One-Time Filter: false +(8 rows) + +select * from (values (1),(2)) v(x) + left join (select q from (select 7 as q from (select where false) ss1) ss2 + left join (select 8 as z) ss3 on true) ss4 on true; + x | q +---+--- + 1 | + 2 | +(2 rows) + -- This example demonstrates the folly of our old "have_dangerous_phv" logic begin; set local from_collapse_limit to 2; diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 3aaa882d668..478c8e59c95 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -1376,6 +1376,16 @@ select * from (select 1 as x) ss1 left join (select 2 as y) ss2 on (true), lateral (select ss2.y as z limit 1) ss3; +-- Also, we mustn't remove an RTE_RESULT that is the only baserel where a PHV +-- can be evaluated, even when the PHV's phrels also mention an outer join. +explain (verbose, costs off) +select * from (values (1),(2)) v(x) + left join (select q from (select 7 as q from (select where false) ss1) ss2 + left join (select 8 as z) ss3 on true) ss4 on true; +select * from (values (1),(2)) v(x) + left join (select q from (select 7 as q from (select where false) ss1) ss2 + left join (select 8 as z) ss3 on true) ss4 on true; + -- This example demonstrates the folly of our old "have_dangerous_phv" logic begin; set local from_collapse_limit to 2; From 9e40d07e140bc21186a8bdd1cb8b35d4d5f65979 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Mon, 20 Jul 2026 12:13:11 +0900 Subject: [PATCH 158/250] Skip unnecessary get_relids_in_jointree() when there are no PHVs Commit 1df9e8d96 made remove_useless_result_rtes() compute the set of baserels in the jointree, to pass down to the find_dependent_phvs() checks. But those checks are no-ops when the query contains no PHVs, since find_dependent_phvs() and find_dependent_phvs_in_jointree() both return early in that case. So we can avoid the get_relids_in_jointree() scan altogether when root->glob->lastPHId is zero, leaving baserels as NULL. Author: Richard Guo Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAMbWs49H275KzgZr3Cd1Hy+6Lmwp35bZ+5PrVc62k3HDLj6hNQ@mail.gmail.com Backpatch-through: 16 --- src/backend/optimizer/prep/prepjointree.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index cc10c5c1ddc..b150a933aad 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -3616,16 +3616,18 @@ report_reduced_full_join(reduce_outer_joins_pass2_state *state2, void remove_useless_result_rtes(PlannerInfo *root) { - Relids baserels; + Relids baserels = NULL; Relids dropped_outer_joins = NULL; ListCell *cell; /* * We'll need the set of baserels in the jointree to perform - * find_dependent_phvs() checks. + * find_dependent_phvs() checks. But if there are no PHVs anywhere in the + * query, those checks are no-ops, so we can skip the work. */ - baserels = get_relids_in_jointree((Node *) root->parse->jointree, - false, false); + if (root->glob->lastPHId != 0) + baserels = get_relids_in_jointree((Node *) root->parse->jointree, + false, false); /* Top level of jointree must always be a FromExpr */ Assert(IsA(root->parse->jointree, FromExpr)); @@ -3695,7 +3697,8 @@ remove_useless_result_rtes(PlannerInfo *root) * (Note that in some cases, parent_quals points to the quals of a parent * more than one level up in the tree.) * - * baserels is the set of base (non-join) RT indexes in the whole jointree. + * baserels is the set of base (non-join) RT indexes in the whole jointree; + * it can be NULL if the query contains no PHVs. */ static Node * remove_useless_results_recurse(PlannerInfo *root, Node *jtnode, @@ -4036,7 +4039,9 @@ remove_result_refs(PlannerInfo *root, int varno, Node *newjtloc) * already decided to remove those joins in remove_useless_result_rtes * and not yet have cleaned their relid bits out of upper PHVs. * But in general, it's the set of baserels that identify possible places - * to evaluate a PHV, and we mustn't let that go to empty. + * to evaluate a PHV, and we mustn't let that go to empty. (The caller is + * allowed to pass baserels as NULL if the query contains no PHVs at all, + * since then there is no work to do anyway.) * * find_dependent_phvs should be used when we want to see if there are * any such PHVs anywhere in the Query. Another use-case is to see if @@ -4048,7 +4053,7 @@ remove_result_refs(PlannerInfo *root, int varno, Node *newjtloc) typedef struct { Relids relids; /* target relid, represented as a relid set */ - Relids baserels; /* set of base (non-OJ) RT indexes in query */ + Relids baserels; /* base RT indexes in query, NULL if no PHVs */ int sublevels_up; /* current nesting level */ } find_dependent_phvs_context; From 19e3aa704126f34f8fa4b36109478a7e6727a5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Mon, 20 Jul 2026 17:21:20 +0200 Subject: [PATCH 159/250] Fix restore of partitions with exclusion constraints Commit 8c852ba9a4 allowed exclusion constraints to be added to partitioned tables, but wasn't careful to verify that pg_restore worked correctly for them. Fix that by making CompareIndexInfo() more selective about what needs to be rejected. Author: Japin Li Reported-by: Keith Paskett Discussion: https://postgr.es/m/2A40921D-83AB-411E-ADA6-7E509A46F1E4@logansw.com --- src/backend/catalog/index.c | 16 ++++++++++++++-- src/test/regress/expected/indexing.out | 15 +++++++++++++++ src/test/regress/sql/indexing.sql | 14 ++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index aa216683b74..fff2fe03e23 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -2645,9 +2645,21 @@ CompareIndexInfo(const IndexInfo *info1, const IndexInfo *info2, return false; } - /* No support currently for comparing exclusion indexes. */ - if (info1->ii_ExclusionOps != NULL || info2->ii_ExclusionOps != NULL) + /* If they're exclusion indexes, their properties must be identical */ + if ((info1->ii_ExclusionOps == NULL) != (info2->ii_ExclusionOps == NULL)) return false; + if (info1->ii_ExclusionOps != NULL) + { + for (i = 0; i < info1->ii_NumIndexKeyAttrs; i++) + { + if (info1->ii_ExclusionOps[i] != info2->ii_ExclusionOps[i]) + return false; + if (info1->ii_ExclusionProcs[i] != info2->ii_ExclusionProcs[i]) + return false; + if (info1->ii_ExclusionStrats[i] != info2->ii_ExclusionStrats[i]) + return false; + } + } return true; } diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out index b8d7d3047be..3e1bbfb74af 100644 --- a/src/test/regress/expected/indexing.out +++ b/src/test/regress/expected/indexing.out @@ -1774,3 +1774,18 @@ reindex index test_pg_index_toast_index; drop index test_pg_index_toast_index; drop function test_pg_index_toast_func; drop table test_pg_index_toast_table; +-- Test of a partitioned index attach, when there are exclusion constraints. +create table idx_excl_part (a int4range, b int4range) partition by list (a); +create table idx_excl_part_1 (a int4range, b int4range); +alter table only idx_excl_part attach partition idx_excl_part_1 for values in ('[0,1)'::int4range); +alter table only idx_excl_part add constraint idxpart_id_data_excl exclude using gist (a with =, b with &&); +alter table idx_excl_part_1 add constraint idxpart_1_id_data_excl exclude using gist (a with &&, b with &&); +-- This should be disallowed, because the constraints don't match. +alter index idxpart_id_data_excl attach partition idxpart_1_id_data_excl; +ERROR: cannot attach index "idxpart_1_id_data_excl" as a partition of index "idxpart_id_data_excl" +DETAIL: The index definitions do not match. +-- but if we recreate the constraint differently, it's allowed: +alter table idx_excl_part_1 drop constraint idxpart_1_id_data_excl; +alter table idx_excl_part_1 add constraint idxpart_1_id_data_excl exclude using gist (a with =, b with &&); +alter index idxpart_id_data_excl attach partition idxpart_1_id_data_excl; +-- leave these tables around, for pg_upgrade testing diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql index 706a6ec04ac..0f302f07a3f 100644 --- a/src/test/regress/sql/indexing.sql +++ b/src/test/regress/sql/indexing.sql @@ -993,3 +993,17 @@ reindex index test_pg_index_toast_index; drop index test_pg_index_toast_index; drop function test_pg_index_toast_func; drop table test_pg_index_toast_table; + +-- Test of a partitioned index attach, when there are exclusion constraints. +create table idx_excl_part (a int4range, b int4range) partition by list (a); +create table idx_excl_part_1 (a int4range, b int4range); +alter table only idx_excl_part attach partition idx_excl_part_1 for values in ('[0,1)'::int4range); +alter table only idx_excl_part add constraint idxpart_id_data_excl exclude using gist (a with =, b with &&); +alter table idx_excl_part_1 add constraint idxpart_1_id_data_excl exclude using gist (a with &&, b with &&); +-- This should be disallowed, because the constraints don't match. +alter index idxpart_id_data_excl attach partition idxpart_1_id_data_excl; +-- but if we recreate the constraint differently, it's allowed: +alter table idx_excl_part_1 drop constraint idxpart_1_id_data_excl; +alter table idx_excl_part_1 add constraint idxpart_1_id_data_excl exclude using gist (a with =, b with &&); +alter index idxpart_id_data_excl attach partition idxpart_1_id_data_excl; +-- leave these tables around, for pg_upgrade testing From 545e2a9d74c3ccca3189e56a515a5793b782eb9b Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Mon, 20 Jul 2026 13:36:39 -0400 Subject: [PATCH 160/250] doc: Granting TRIGGER or REFERENCES on table is dangerous. It's always been the case that granting these privileges to users that you don't fully trust was a bad idea, but it hasn't always been obvious to people reading the documentation that this is the case. To prevent confusion, and also repeated reports to pgsql-security, mention it explicitly. Discussion: http://postgr.es/m/CA+TgmobrjCHBuWHrvX3=2vndUCO2thUOdevrCcMDFW86cqCYvw@mail.gmail.com Reviewed-by: Nathan Bossart Backpatch-through: 14 --- doc/src/sgml/ddl.sgml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 8813a09561b..ddd7096f6b2 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2092,7 +2092,11 @@ REVOKE ALL ON accounts FROM PUBLIC; Allows creation of a foreign key constraint referencing a - table, or specific column(s) of a table. + table, or specific column(s) of a table. Great care should be taken when + granting this privilege, since a user who creates a foreign key can arrange + for enforcement of that foreign key to call an arbitrary function, such as + a cast function, and such functions will be called with the privileges of + the table owner. @@ -2101,7 +2105,9 @@ REVOKE ALL ON accounts FROM PUBLIC; TRIGGER - Allows creation of a trigger on a table, view, etc. + Allows creation of a trigger on a table, view, etc. Great care should be + taken when granting this privilege, since any triggers added to a table + or view will be executed with the privileges of users who modify it. From 1d299d6abfcdcd9400dd1e14b4213a9f39e41691 Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Wed, 22 Jul 2026 08:47:44 -0400 Subject: [PATCH 161/250] walsummarizer: Guard against WAL files whose tail ends are not valid. SummarizeWAL documents that maximum_lsn should be passed as "the switch point when reading a historic timeline, or the most-recently-measured end of WAL when reading the current timeline." But the caller always passed the most recently measured end-of-WAL even when reading from a historic timeline, due to an oversight on my part. Fix that. As far as I can determine, for this to become an issue in practice, it's necessary to have a corrupted WAL file in the archive. SummarizeWAL checks that every record it processes both starts and ends before switch_lsn; so if all the WAL files in the archive are valid, SummarizeWAL will still discover where it should stop summarizing and do the right thing. However, if there's a corrupted file in the WAL archive, and if it is also the case that the end of the current timeline has advanced past the switch point, then the incorrect maximum_lsn value can result in trying to read an invalid record and erroring out, which leads repeatedly retrying and failing with an error every time. One way this could occur is if a new primary is promoted and creates a .partial file, and the user manually renames that file to remove the suffix, and it is then archived. In that situation, the tail end of the file need not be valid WAL, and that could lead to a stuck WAL summarizer. Reported-by: Fabrice Chapuis Analyzed-by: Thom Brown (using claude) Discussion: http://postgr.es/m/CAA5-nLDdvGMkN6Z-GaHGHG5T7QWEgv4YoHO7XvOJbeD00cghNg@mail.gmail.com Backpatch-through: 17 --- src/backend/postmaster/walsummarizer.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index e0ff745f2dd..eec6ca08d5c 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -353,6 +353,7 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) { XLogRecPtr latest_lsn; TimeLineID latest_tli; + XLogRecPtr maximum_lsn; XLogRecPtr end_of_summary_lsn; /* Flush any leaked data in the top-level context */ @@ -417,9 +418,10 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) } /* Summarize WAL. */ + maximum_lsn = XLogRecPtrIsValid(switch_lsn) ? switch_lsn : latest_lsn; end_of_summary_lsn = SummarizeWAL(current_tli, current_lsn, exact, - switch_lsn, latest_lsn); + switch_lsn, maximum_lsn); Assert(!XLogRecPtrIsInvalid(end_of_summary_lsn)); Assert(end_of_summary_lsn >= current_lsn); From bb0a3ca8d218b6c0792235d7306117e5bc36294a Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 23 Jul 2026 14:37:44 +0900 Subject: [PATCH 162/250] injection_points: Clear waiter slot on error and exit injection_wait() only clears its slot in the waiter array after the wait loop finishes. When the waiting query is canceled or the backend is terminated (wait look has a CHECK_FOR_INTERRUPS), the slot leaks. Later wakeups of the same point then bump the counter of the leaked slot instead of the real waiter, that sleeps forever. Repeated leaks can exhaust all the slots. The code is changed so as the waiting loop is wrapped with PG_ENSURE_ERROR_CLEANUP, so as the injection point slots, that are shared resources, can be cleaned up on ERROR as much as a FATAL. An isolation test is added: cancel one waiter, terminate another waiter, then check that a later waiter still receives a wakeup. Without the fixed code, the test would fail on timeout. Author: Zsolt Parragi Discussion: https://postgr.es/m/CAN4CZFO+KF=cc0-iEg28RhqRBp_fTs6D4b8b7D7DB-pGYP3Ccg@mail.gmail.com Backpatch-through: 17 --- src/test/modules/injection_points/Makefile | 1 + .../expected/wait_cleanup.out | 87 +++++++++++++++++++ .../injection_points/injection_points.c | 39 ++++++--- src/test/modules/injection_points/meson.build | 1 + .../injection_points/specs/wait_cleanup.spec | 50 +++++++++++ 5 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 src/test/modules/injection_points/expected/wait_cleanup.out create mode 100644 src/test/modules/injection_points/specs/wait_cleanup.spec diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index 2e49d30f942..e8938596772 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -17,6 +17,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ inplace \ syscache-update-pruned \ + wait_cleanup \ heap_lock_update TAP_TESTS = 1 diff --git a/src/test/modules/injection_points/expected/wait_cleanup.out b/src/test/modules/injection_points/expected/wait_cleanup.out new file mode 100644 index 00000000000..c5be17428fc --- /dev/null +++ b/src/test/modules/injection_points/expected/wait_cleanup.out @@ -0,0 +1,87 @@ +Parsed test spec with 3 sessions + +starting permutation: wait1 cancel3 noop3 wait2 wakeup3 noop2 detach3 +injection_points_attach +----------------------- + +(1 row) + +step wait1: SELECT injection_points_run('injection-points-wait'); +step cancel3: + SELECT pg_cancel_backend(pid) FROM pg_stat_activity + WHERE wait_event = 'injection-points-wait'; + +step wait1: <... completed> +ERROR: canceling statement due to user request +step cancel3: <... completed> +pg_cancel_backend +----------------- +t +(1 row) + +step noop3: +step wait2: SELECT injection_points_run('injection-points-wait'); +step wakeup3: SELECT injection_points_wakeup('injection-points-wait'); +injection_points_wakeup +----------------------- + +(1 row) + +step wait2: <... completed> +injection_points_run +-------------------- + +(1 row) + +step noop2: +step detach3: SELECT injection_points_detach('injection-points-wait'); +injection_points_detach +----------------------- + +(1 row) + + +starting permutation: wait1 terminate3 noop3 wait2 wakeup3 noop2 detach3 +injection_points_attach +----------------------- + +(1 row) + +step wait1: SELECT injection_points_run('injection-points-wait'); +step terminate3: + SELECT pg_terminate_backend(pid) FROM pg_stat_activity + WHERE wait_event = 'injection-points-wait'; + +step wait1: <... completed> +FATAL: terminating connection due to administrator command +server closed the connection unexpectedly + This probably means the server terminated abnormally + before or while processing the request. + +step terminate3: <... completed> +pg_terminate_backend +-------------------- +t +(1 row) + +step noop3: +step wait2: SELECT injection_points_run('injection-points-wait'); +step wakeup3: SELECT injection_points_wakeup('injection-points-wait'); +injection_points_wakeup +----------------------- + +(1 row) + +step wait2: <... completed> +injection_points_run +-------------------- + +(1 row) + +step noop2: +step detach3: SELECT injection_points_detach('injection-points-wait'); +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c index 71b1bd0473f..c493cfda9cc 100644 --- a/src/test/modules/injection_points/injection_points.c +++ b/src/test/modules/injection_points/injection_points.c @@ -254,6 +254,19 @@ injection_notice(const char *name, const void *private_data, void *arg) elog(NOTICE, "notice triggered for injection point %s", name); } +/* + * Error cleanup callback for injection point waits. + */ +static void +injection_wait_cleanup(int code, Datum arg) +{ + int index = DatumGetInt32(arg); + + SpinLockAcquire(&inj_state->lock); + inj_state->name[index][0] = '\0'; + SpinLockRelease(&inj_state->lock); +} + /* Wait on a condition variable, awaken by injection_points_wakeup() */ void injection_wait(const char *name, const void *private_data, void *arg) @@ -300,24 +313,26 @@ injection_wait(const char *name, const void *private_data, void *arg) /* And sleep.. */ ConditionVariablePrepareToSleep(&inj_state->wait_point); - for (;;) + PG_ENSURE_ERROR_CLEANUP(injection_wait_cleanup, Int32GetDatum(index)); { - uint32 new_wait_counts; + for (;;) + { + uint32 new_wait_counts; - SpinLockAcquire(&inj_state->lock); - new_wait_counts = inj_state->wait_counts[index]; - SpinLockRelease(&inj_state->lock); + SpinLockAcquire(&inj_state->lock); + new_wait_counts = inj_state->wait_counts[index]; + SpinLockRelease(&inj_state->lock); - if (old_wait_counts != new_wait_counts) - break; - ConditionVariableSleep(&inj_state->wait_point, injection_wait_event); + if (old_wait_counts != new_wait_counts) + break; + ConditionVariableSleep(&inj_state->wait_point, injection_wait_event); + } + ConditionVariableCancelSleep(); } - ConditionVariableCancelSleep(); + PG_END_ENSURE_ERROR_CLEANUP(injection_wait_cleanup, Int32GetDatum(index)); /* Remove this injection point from the waiters. */ - SpinLockAcquire(&inj_state->lock); - inj_state->name[index][0] = '\0'; - SpinLockRelease(&inj_state->lock); + injection_wait_cleanup(0, Int32GetDatum(index)); } /* diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index a73e1fe6c34..c25768b5e21 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -48,6 +48,7 @@ tests += { 'basic', 'inplace', 'syscache-update-pruned', + 'wait_cleanup', 'heap_lock_update', ], 'runningcheck': false, # see syscache-update-pruned diff --git a/src/test/modules/injection_points/specs/wait_cleanup.spec b/src/test/modules/injection_points/specs/wait_cleanup.spec new file mode 100644 index 00000000000..ed7d21c4de4 --- /dev/null +++ b/src/test/modules/injection_points/specs/wait_cleanup.spec @@ -0,0 +1,50 @@ +# Check that a canceled or terminated waiter does not leave a stale slot +# behind in the waiter array. A leaked slot would make later wakeups of +# the same injection point bump the leaked slot's counter instead of the +# real waiter's, leaving the real waiter stuck. + +setup +{ + CREATE EXTENSION injection_points; +} +teardown +{ + DROP EXTENSION injection_points; +} + +# The first waiter, that gets canceled or terminated. This does not +# use injection_points_set_local() on purpose: the injection point +# must survive s1's termination so that s3 can still detach it. +session s1 +setup { + SELECT injection_points_attach('injection-points-wait', 'wait'); +} +step wait1 { SELECT injection_points_run('injection-points-wait'); } + +# The second waiter, that receives a wakeup. +session s2 +step wait2 { SELECT injection_points_run('injection-points-wait'); } +step noop2 { } + +# Control session. The blocker annotations on cancel3/terminate3, +# together with noop3, make the tester wait until wait1 has fully +# completed before starting wait2. Otherwise, wait2 could register a +# new waiter slot while s1 still owns the previous one. +session s3 +step cancel3 { + SELECT pg_cancel_backend(pid) FROM pg_stat_activity + WHERE wait_event = 'injection-points-wait'; +} +step terminate3 { + SELECT pg_terminate_backend(pid) FROM pg_stat_activity + WHERE wait_event = 'injection-points-wait'; +} +step wakeup3 { SELECT injection_points_wakeup('injection-points-wait'); } +step detach3 { SELECT injection_points_detach('injection-points-wait'); } +step noop3 { } + +permutation wait1 cancel3(wait1) noop3 wait2 wakeup3 noop2 detach3 + +# The terminate permutation has to stay last: s1's connection is dead +# afterwards, and the tester never reconnects a session. +permutation wait1 terminate3(wait1) noop3 wait2 wakeup3 noop2 detach3 From f2d6cf880240b1dcbb8791c2687b017bff23b156 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Sat, 25 Jul 2026 19:09:19 +0900 Subject: [PATCH 163/250] psql: Allow pg_read_all_stats to see database size in \l+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pg_database_size() allows access to users who have either CONNECT privilege on the target database or privileges of the pg_read_all_stats role. However, previously, psql's \l+ checked only for CONNECT, so users with privileges of pg_read_all_stats still saw "No Access" for databases they could not connect to. Fix this by making \l+ also check pg_has_role('pg_read_all_stats', 'USAGE'), matching pg_database_size()'s permission rules. For back branches, emit the pg_read_all_stats check only when connected to PostgreSQL 10 or later, since earlier releases do not have that predefined role. Backpatch to all supported versions. Author: Christoph Berg Reviewed-by: Álvaro Herrera Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/amCo6qRmnfPVk4-V@msg.df7cb.de Backpatch-through: 14 --- doc/src/sgml/ref/psql-ref.sgml | 5 +++-- src/bin/psql/describe.c | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index bc273f8dce5..e3188660a3d 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2813,8 +2813,9 @@ SELECT are displayed in expanded mode. If + is appended to the command name, database sizes, default tablespaces, and descriptions are also displayed. - (Size information is only available for databases that the current - user can connect to.) + Size information is available for databases on which the current user has + CONNECT privilege, or if the current user is a superuser + or has privileges of the pg_read_all_stats role. diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index dd25d2fe7b8..31f12f89439 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -999,16 +999,20 @@ listAllDbs(const char *pattern, bool verbose) appendPQExpBufferStr(&buf, " "); printACLColumn(&buf, "d.datacl"); if (verbose) + { appendPQExpBuffer(&buf, ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + " %s" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" ",\n t.spcname as \"%s\"" ",\n pg_catalog.shobj_description(d.oid, 'pg_database') as \"%s\"", + pset.sversion >= 100000 ? "OR pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" : "", gettext_noop("Size"), gettext_noop("Tablespace"), gettext_noop("Description")); + } appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_database d\n"); if (verbose) From d560e730e813343b8d3f4a336244f2bc09ca84fc Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Sat, 25 Jul 2026 12:01:32 -0400 Subject: [PATCH 164/250] Fix another empty nbtree index SSI race. Commit f9b7fc65 fixed a race when predicate-locking completely empty btrees: without a buffer lock held, a matching key could be inserted between _bt_search and the PredicateLockRelation call, so the scan would miss concurrently inserted tuples while the writer wouldn't see the reader's predicate lock. That commit only fixed _bt_first's _bt_search path, though. Scans without useful insertion scan keys return early from _bt_first via _bt_endpoint, which still didn't recheck if the relation was empty. To fix, add handling to _bt_endpoint that is analogous to the handling added to _bt_search by commit f9b7fc65. Author: Peter Geoghegan Discussion: https://postgr.es/m/CAH2-WzkNoTn3yXY0iGkSuavJ+sL8EROf+kitW+_2v2tJVWuKmA@mail.gmail.com Backpatch-through: 14 --- src/backend/access/nbtree/nbtsearch.c | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c index 47b6552d67c..d47e4b20783 100644 --- a/src/backend/access/nbtree/nbtsearch.c +++ b/src/backend/access/nbtree/nbtsearch.c @@ -2714,12 +2714,21 @@ _bt_endpoint(IndexScanDesc scan, ScanDirection dir) if (!BufferIsValid(so->currPos.buf)) { /* - * Empty index. Lock the whole relation, as nothing finer to lock - * exists. + * Empty index. Lock the whole relation using the approach explained + * at the same point in the _bt_first path. */ - PredicateLockRelation(rel, scan->xs_snapshot); - _bt_parallel_done(scan); - return false; + if (IsolationIsSerializable()) + { + PredicateLockRelation(rel, scan->xs_snapshot); + so->currPos.buf = _bt_get_endpoint(rel, 0, + ScanDirectionIsBackward(dir)); + } + + if (!BufferIsValid(so->currPos.buf)) + { + _bt_parallel_done(scan); + return false; + } } page = BufferGetPage(so->currPos.buf); From e4527519b77e0f158e452fcbbcb2ac902d01ad44 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 28 Jul 2026 08:35:12 +0900 Subject: [PATCH 165/250] Fix propagation of indimmediate flag in index_create_copy() index_create_copy is used to create copy definitions of existing indexes. Currently, it passes 0 as constr_flags to index_create(), which results in the copied index to always be created as immediate (indimmediate set to true). For deferrable unique constraints, it means that the transient index used during the phase 2 of REINDEX CONCURRENTLY forces immediate constraint checks on concurrent inserts, which can cause unexpected constraint violations based on the definition of the parent table, inconsistently set in the copied index. To fix this without violating the contract of constr_flags (which should only be used when creating constraints) and without relaxing the strict assertion in index_create(), this introduces a new index creation flag: INDEX_CREATE_DEFERRABLE. If set, a copied index's indimmediate is set to false, meaning that unique constraints are not enforced immediately on insertion, but at transaction commit time. An isolation test for REINDEX CONCURRENTLY is added, based on an injection point waiting after phase 1 of the operation, where an index copy has been built and is able to accept DMLs for its validation in phase 2. The test is tentatively backpatched down to v17. INJECTION_POINT() is outside a transaction context, which should be fine on HEAD since 8daeaa9b642c but I suspect may cause issues in v19 and older branches due to the wait facility depending on condition variables and a DSM setup, but let's see what the buildfarm tells. Author: Nitin Motiani Discussion: https://postgr.es/m/CAH5HC97JmjPpgiQOqW9xm8qXhNiu7zZ1Qh+FfhEESJuDv69kuQ@mail.gmail.com Backpatch-through: 14 --- src/backend/catalog/index.c | 20 ++++++-- src/backend/commands/indexcmds.c | 2 + src/include/catalog/index.h | 1 + src/test/modules/injection_points/Makefile | 1 + .../reindex_concurrently_deferred.out | 41 +++++++++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/reindex_concurrently_deferred.spec | 50 +++++++++++++++++++ 7 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/test/modules/injection_points/expected/reindex_concurrently_deferred.out create mode 100644 src/test/modules/injection_points/specs/reindex_concurrently_deferred.spec diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index fff2fe03e23..5f9ebc158b9 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -714,6 +714,10 @@ UpdateIndexRelation(Oid indexoid, * already exists. * INDEX_CREATE_PARTITIONED: * create a partitioned index (table must be partitioned) + * INDEX_CREATE_DEFERRABLE: + * index supports a deferrable constraint, mark it as + * non-immediate (indimmediate = false). + * * constr_flags: flags passed to index_constraint_create * (only if INDEX_CREATE_ADD_CONSTRAINT is set) * allow_system_table_mods: allow table to be a system catalog @@ -1046,7 +1050,8 @@ index_create(Relation heapRelation, indexInfo, collationIds, opclassIds, coloptions, isprimary, is_exclusion, - (constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) == 0, + (constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) == 0 && + (flags & INDEX_CREATE_DEFERRABLE) == 0, !concurrent && !invalid, !concurrent); @@ -1317,6 +1322,8 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, List *indexColNames = NIL; List *indexExprs = NIL; List *indexPreds = NIL; + bits16 flags = INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT; + Form_pg_index indexForm; indexRelation = index_open(oldIndexId, RowExclusiveLock); @@ -1336,6 +1343,13 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldIndexId)); if (!HeapTupleIsValid(indexTuple)) elog(ERROR, "cache lookup failed for index %u", oldIndexId); + + indexForm = (Form_pg_index) GETSTRUCT(indexTuple); + + /* Old index is deferrable, do the same for the new index */ + if (!indexForm->indimmediate) + flags |= INDEX_CREATE_DEFERRABLE; + indclassDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple, Anum_pg_index_indclass); indclass = (oidvector *) DatumGetPointer(indclassDatum); @@ -1458,8 +1472,8 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, indcoloptions->values, stattargets, reloptionsDatum, - INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT, - 0, + flags, + 0, /* constr_flags */ true, /* allow table to be a system catalog? */ false, /* is_internal? */ NULL); diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index fd083e5ed25..faf74bfe200 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -4129,6 +4129,8 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein CommitTransactionCommand(); } + INJECTION_POINT("reindex-conc-index-built", NULL); + StartTransactionCommand(); /* diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h index 4daa8bef5ee..60430aee0d5 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -65,6 +65,7 @@ extern void index_check_primary_key(Relation heapRel, #define INDEX_CREATE_IF_NOT_EXISTS (1 << 4) #define INDEX_CREATE_PARTITIONED (1 << 5) #define INDEX_CREATE_INVALID (1 << 6) +#define INDEX_CREATE_DEFERRABLE (1 << 7) extern Oid index_create(Relation heapRelation, const char *indexRelationName, diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index e8938596772..f2ab4cde214 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -16,6 +16,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ inplace \ + reindex_concurrently_deferred \ syscache-update-pruned \ wait_cleanup \ heap_lock_update diff --git a/src/test/modules/injection_points/expected/reindex_concurrently_deferred.out b/src/test/modules/injection_points/expected/reindex_concurrently_deferred.out new file mode 100644 index 00000000000..39924fa24fe --- /dev/null +++ b/src/test/modules/injection_points/expected/reindex_concurrently_deferred.out @@ -0,0 +1,41 @@ +Parsed test spec with 2 sessions + +starting permutation: reindex check_catalog begin2 write2 write_dup resolve_dup commit2 wakeup noop1 +injection_points_attach +----------------------- + +(1 row) + +step reindex: REINDEX TABLE CONCURRENTLY reind_deferred; +step check_catalog: + SELECT c.relname, i.indisunique, i.indimmediate, i.indisready, i.indisvalid + FROM pg_class c + JOIN pg_index i ON i.indexrelid = c.oid + WHERE c.relname = 'uq_val_ccnew'; + +relname |indisunique|indimmediate|indisready|indisvalid +------------+-----------+------------+----------+---------- +uq_val_ccnew|t |f |t |f +(1 row) + +step begin2: BEGIN; +step write2: INSERT INTO reind_deferred VALUES (3, 9); +step write_dup: INSERT INTO reind_deferred VALUES (4, 9); +step resolve_dup: UPDATE reind_deferred SET val = 10 WHERE id = 4; +step commit2: COMMIT; +step wakeup: + SELECT injection_points_detach('reindex-conc-index-built'); + SELECT injection_points_wakeup('reindex-conc-index-built'); + +injection_points_detach +----------------------- + +(1 row) + +injection_points_wakeup +----------------------- + +(1 row) + +step reindex: <... completed> +step noop1: diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index c25768b5e21..2eec3d0935d 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -47,6 +47,7 @@ tests += { 'specs': [ 'basic', 'inplace', + 'reindex_concurrently_deferred', 'syscache-update-pruned', 'wait_cleanup', 'heap_lock_update', diff --git a/src/test/modules/injection_points/specs/reindex_concurrently_deferred.spec b/src/test/modules/injection_points/specs/reindex_concurrently_deferred.spec new file mode 100644 index 00000000000..4b95e1da2a7 --- /dev/null +++ b/src/test/modules/injection_points/specs/reindex_concurrently_deferred.spec @@ -0,0 +1,50 @@ +# REINDEX CONCURRENTLY with DEFERRED constraints +# +# Verify that concurrent writes that temporarily violate a deferred unique +# constraint do not fail while REINDEX CONCURRENTLY is running. +# +# The injection point "reindex-conc-index-built" fires after the phase 2 +# of REINDEX CONCURRENTLY, when the new index has indisready = true (inserts +# are checked against it) but indisvalid = false. + +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE reind_deferred (id int, val int, + CONSTRAINT uq_val UNIQUE(val) DEFERRABLE INITIALLY DEFERRED); + INSERT INTO reind_deferred VALUES (1, 1), (2, 2); +} + +teardown +{ + DROP TABLE reind_deferred; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('reindex-conc-index-built', 'wait'); +} +step reindex { REINDEX TABLE CONCURRENTLY reind_deferred; } +step noop1 { } + +session s2 +step check_catalog { + SELECT c.relname, i.indisunique, i.indimmediate, i.indisready, i.indisvalid + FROM pg_class c + JOIN pg_index i ON i.indexrelid = c.oid + WHERE c.relname = 'uq_val_ccnew'; +} +step begin2 { BEGIN; } +step write2 { INSERT INTO reind_deferred VALUES (3, 9); } +step write_dup { INSERT INTO reind_deferred VALUES (4, 9); } +step resolve_dup { UPDATE reind_deferred SET val = 10 WHERE id = 4; } +step commit2 { COMMIT; } +step wakeup { + SELECT injection_points_detach('reindex-conc-index-built'); + SELECT injection_points_wakeup('reindex-conc-index-built'); +} + +permutation reindex check_catalog begin2 write2 write_dup resolve_dup commit2 wakeup noop1 From 7becb647da743fc3ca059181fef94419cd8167d6 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Tue, 28 Jul 2026 10:50:13 +0200 Subject: [PATCH 166/250] Restore vacuum_delay_point() in GIN posting-tree leaf vacuum Commit fd83c83d094 turned the recursive posting-tree cleanup in ginVacuumPostingTreeLeaves() into an iterative sweep that follows the tree's leaf pages via their rightlinks. The recursive version called vacuum_delay_point() while processing the tree, but that call was removed and never re-added to the new loop. As that commit only set out to fix a deadlock, the removal appears to have been unintentional. Consequently the leaf-page sweep of a single posting tree runs with no vacuum_delay_point(), and therefore no CHECK_FOR_INTERRUPTS(). A posting tree stores all the TIDs for one indexed key, so for a frequently occurring key it can span a large number of leaf pages. While such a tree is being vacuumed the operation ignores vacuum_cost_delay and does not respond to query cancellation or statement_timeout; an autovacuum worker likewise cannot be interrupted mid-sweep when another backend requests a conflicting lock. Restore the call, placed after the current page has been unlocked and released so that no buffer content lock is held across a potential delay (cf. 21c27af65fb). The sibling loops in ginbulkdelete() and ginvacuumcleanup() already call vacuum_delay_point() once per page. Author: Paul Kim Co-authored-by: Alexander Korotkov Reviewed-by: Michael Paquier Reviewed-by: Andrey Borodin Reviewed-by: solai v Discussion: https://postgr.es/m/178447127453.110.12276981925360691905%40mail.gmail.com Backpatch-through: 14 --- src/backend/access/gin/ginvacuum.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c index fbbe3a6dd70..adac8cd4267 100644 --- a/src/backend/access/gin/ginvacuum.c +++ b/src/backend/access/gin/ginvacuum.c @@ -395,6 +395,13 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno) if (blkno == InvalidBlockNumber) break; + /* + * A safe point to delay/accept interrupts: the previous page has been + * unlocked and released, so we hold no buffer content lock (nor any + * other LWLock) here and CHECK_FOR_INTERRUPTS() can do its job. + */ + vacuum_delay_point(false); + buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno, RBM_NORMAL, gvs->strategy); LockBuffer(buffer, GIN_EXCLUSIVE); From 73d63d1c1f676f4fcb289cdf9d052881719eb02f Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Tue, 28 Jul 2026 10:39:43 -0700 Subject: [PATCH 167/250] Fix pg_get_publication_tables() failure with concurrent DROP TABLE. pg_get_publication_tables() collects the OIDs of the published tables on its first call, without locking them, and then reopens each table later, once per result row, to compute its column list and fetch its row filter. The reopen used table_open(), which errors out with "could not open relation with OID" if the table has been dropped in the meantime. This could happen for any published table without an explicit column list, which is every table in FOR ALL TABLES and FOR TABLES IN SCHEMA publications, but also FOR TABLE entries without a column list. The failure is common in environments where many tables are created and dropped while publication tables are being queried, e.g. by table synchronization on a subscriber. Fix by opening every table with try_table_open(), which returns NULL if the relation no longer exists, and skipping the table in that case. Concurrently dropped tables are thus simply absent from the result set, which is the expected point-in-time behavior. As a side effect, tables with an explicit column list, which were previously returned without being opened, are now also locked with AccessShareLock, so the function can block behind concurrent DDL on such tables where it previously did not. Backpatch to v16, where we added the table_open() call in pg_get_publication_tables(). Author: Bharath Rupireddy Reviewed-by: Bertrand Drouvot Reviewed-by: shveta malik Reviewed-by: Ajin Cherian Reviewed-by: Masahiko Sawada Reviewed-by: Chao Li Discussion: https://www.postgresql.org/message-id/CALj2ACVYYooWH-5tJ6cPKkU%2BmutVxwb_z4S%2BqAi-zdrFqxXE2Q%40mail.gmail.com Backpatch-through: 16 --- src/backend/catalog/pg_publication.c | 52 +++++++++++++++---- .../expected/pub-concurrent-drop.out | 16 ++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/pub-concurrent-drop.spec | 36 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 src/test/isolation/expected/pub-concurrent-drop.out create mode 100644 src/test/isolation/specs/pub-concurrent-drop.spec diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index d6f94db5d99..259ecab1688 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -1117,14 +1117,27 @@ Datum pg_get_publication_tables(PG_FUNCTION_ARGS) { #define NUM_PUBLICATION_TABLES_ELEM 4 + + /* + * State carried across SRF calls. We track the index ourselves instead of + * using funcctx->call_cntr, so that concurrently dropped tables can be + * skipped without emitting a row. + */ + typedef struct + { + List *table_infos; /* list of published_rel */ + int curr_idx; /* current index into table_infos */ + } publication_tables_state; + FuncCallContext *funcctx; - List *table_infos = NIL; + publication_tables_state *ptstate = NULL; /* stuff done only on the first call of the function */ if (SRF_IS_FIRSTCALL()) { TupleDesc tupdesc; MemoryContext oldcontext; + List *table_infos = NIL; ArrayType *arr; Datum *elems; int nelems, @@ -1222,26 +1235,47 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) PG_NODE_TREEOID, -1, 0); funcctx->tuple_desc = BlessTupleDesc(tupdesc); - funcctx->user_fctx = table_infos; + + /* Store the state to be used across SRF calls. */ + ptstate = palloc_object(publication_tables_state); + ptstate->table_infos = table_infos; + ptstate->curr_idx = 0; + funcctx->user_fctx = ptstate; MemoryContextSwitchTo(oldcontext); } /* stuff done on every call of the function */ funcctx = SRF_PERCALL_SETUP(); - table_infos = (List *) funcctx->user_fctx; + ptstate = (publication_tables_state *) funcctx->user_fctx; - if (funcctx->call_cntr < list_length(table_infos)) + while (ptstate->curr_idx < list_length(ptstate->table_infos)) { HeapTuple pubtuple = NULL; HeapTuple rettuple; Publication *pub; - published_rel *table_info = (published_rel *) list_nth(table_infos, funcctx->call_cntr); + published_rel *table_info = (published_rel *) list_nth(ptstate->table_infos, + ptstate->curr_idx); Oid relid = table_info->relid; - Oid schemaid = get_rel_namespace(relid); + Relation rel; + Oid schemaid; Datum values[NUM_PUBLICATION_TABLES_ELEM] = {0}; bool nulls[NUM_PUBLICATION_TABLES_ELEM] = {0}; + /* Advance the index for the next call. */ + ptstate->curr_idx++; + + /* + * The table OIDs were collected earlier, so a table may have been + * dropped before we get here. try_table_open() returns NULL if it is + * already gone, in which case we skip it; such tables are simply + * absent from the result set, which is the expected point-in-time + * behavior. + */ + rel = try_table_open(relid, AccessShareLock); + if (rel == NULL) + continue; + /* * Form tuple with appropriate data. */ @@ -1255,6 +1289,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) * We don't consider row filters or column lists for FOR ALL TABLES or * FOR TABLES IN SCHEMA publications. */ + schemaid = RelationGetNamespace(rel); if (!pub->alltables && !SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP, ObjectIdGetDatum(schemaid), @@ -1284,7 +1319,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) /* Show all columns when the column list is not specified. */ if (nulls[2]) { - Relation rel = table_open(relid, AccessShareLock); int nattnums = 0; int16 *attnums; TupleDesc desc = RelationGetDescr(rel); @@ -1321,10 +1355,10 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) values[2] = PointerGetDatum(buildint2vector(attnums, nattnums)); nulls[2] = false; } - - table_close(rel, AccessShareLock); } + table_close(rel, AccessShareLock); + rettuple = heap_form_tuple(funcctx->tuple_desc, values, nulls); SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(rettuple)); diff --git a/src/test/isolation/expected/pub-concurrent-drop.out b/src/test/isolation/expected/pub-concurrent-drop.out new file mode 100644 index 00000000000..8360af0ec9c --- /dev/null +++ b/src/test/isolation/expected/pub-concurrent-drop.out @@ -0,0 +1,16 @@ +Parsed test spec with 2 sessions + +starting permutation: lock list_pub_tables drop_and_commit +step lock: BEGIN; LOCK pubdrop.dropme IN ACCESS EXCLUSIVE MODE; +step list_pub_tables: + SELECT relid::regclass AS tablename + FROM pg_get_publication_tables('pub_schema') + ORDER BY tablename; + +step drop_and_commit: DROP TABLE pubdrop.dropme; COMMIT; +step list_pub_tables: <... completed> +tablename +-------------- +pubdrop.keepme +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 204b9e399c6..9741b881f02 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -118,3 +118,4 @@ test: serializable-parallel-3 test: matview-write-skew test: lock-nowait test: ddl-dependency-locking +test: pub-concurrent-drop diff --git a/src/test/isolation/specs/pub-concurrent-drop.spec b/src/test/isolation/specs/pub-concurrent-drop.spec new file mode 100644 index 00000000000..4f7d701d60c --- /dev/null +++ b/src/test/isolation/specs/pub-concurrent-drop.spec @@ -0,0 +1,36 @@ +# Tests for concurrently dropping a relation while a publication's tables are +# being listed. + +setup +{ + CREATE SCHEMA pubdrop; + CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA pubdrop; + CREATE TABLE pubdrop.dropme (id int); + CREATE TABLE pubdrop.keepme (id int); +} + +teardown +{ + DROP SCHEMA pubdrop CASCADE; + DROP PUBLICATION pub_schema; +} + +session s1 +step lock { BEGIN; LOCK pubdrop.dropme IN ACCESS EXCLUSIVE MODE; } +step drop_and_commit { DROP TABLE pubdrop.dropme; COMMIT; } + +session s2 +step list_pub_tables +{ + SELECT relid::regclass AS tablename + FROM pg_get_publication_tables('pub_schema') + ORDER BY tablename; +} + +# Hold an ACCESS EXCLUSIVE lock on the table in one session, so that the query +# listing a publication's tables in another session blocks when it tries to +# open the locked table. Then drop the table in the same lock-holding session +# and commit, releasing the lock, so the query in another session resumes and +# skips the now-dropped table instead of erroring with "could not open relation +# with OID". +permutation lock list_pub_tables drop_and_commit diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 938befd5c80..125e81d81bc 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3974,6 +3974,7 @@ pthread_mutex_t pthread_once_t pthread_t ptrdiff_t +publication_tables_state published_rel pull_var_clause_context pull_varattnos_context From b563fc6bd926fd9dca86d0c1a16fbf2e3e63dee9 Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Tue, 28 Jul 2026 12:33:35 -0700 Subject: [PATCH 168/250] Fix logical decoding of empty prepared transactions. A two-phase transaction that is assigned an XID but produces no change to be decoded -- for example, one that only acquires row locks via SELECT ... FOR SHARE -- has no base snapshot in the reorder buffer. ReorderBufferReplay() already skips such a transaction at PREPARE time and never invokes the begin_prepare/change/prepare callbacks for it, but ReorderBufferFinishPrepared() still called the commit_prepared (or rollback_prepared) callback. As a result a spurious COMMIT/ROLLBACK PREPARED was sent to the output plugin with no preceding PREPARE. For the built-in subscriber this breaks replication (the apply worker fails to find the prepared transaction), and test_decoding could even crash. Fix this by detecting an empty transaction (base_snapshot == NULL) in ReorderBufferFinishPrepared() and cleaning it up without invoking the commit/rollback prepared callbacks, mirroring the existing empty transaction handling in ReorderBufferReplay(). On v18 and newer versions, commit 072ee847ad4 changed ReorderBufferPrepare() to send the prepare whenever it had not already been sent, which also fires for empty transactions and emits a spurious PREPARE. On those branches ReorderBufferPrepare() is therefore additionally guarded with base_snapshot != NULL. This guard and the Assert(!rbtxn_sent_prepare()) added in ReorderBufferFinishPrepared(), are not necessary on v17 and older versions: there ReorderBufferPrepare() only sends a prepare for concurrently-aborted transactions (which never applies to an empty transaction) and the RBTXN_SENT_PREPARE flag does not exist. Back-patch to v14, where decoding of two-phase transactions was introduced. Bug: #19556 Reported-by: Alexander Kozhemyakin Reviewed-by: Amit Kapila Discussion: https://postgr.es/m/19556-daa6d7ea65054d48@postgresql.org Backpatch-through: 14 --- contrib/test_decoding/expected/twophase.out | 25 +++++++++++ contrib/test_decoding/sql/twophase.sql | 12 ++++++ .../replication/logical/reorderbuffer.c | 34 +++++++++++++-- src/test/subscription/t/021_twophase.pl | 41 +++++++++++++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/contrib/test_decoding/expected/twophase.out b/contrib/test_decoding/expected/twophase.out index 08a7c56b5df..ea3c51f8215 100644 --- a/contrib/test_decoding/expected/twophase.out +++ b/contrib/test_decoding/expected/twophase.out @@ -227,6 +227,31 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc COMMIT PREPARED 'test_toast_table_access' (1 row) +-- Test that an empty prepared transaction should not be decoded, whether it +-- is committed or rolled back. +BEGIN; +SELECT * FROM test_prepared1 WHERE id = 1 FOR SHARE; + id | data +----+------ + 1 | +(1 row) + +PREPARE TRANSACTION 'test_empty_transaction'; +COMMIT PREPARED 'test_empty_transaction'; +BEGIN; +SELECT * FROM test_prepared1 WHERE id = 1 FOR SHARE; + id | data +----+------ + 1 | +(1 row) + +PREPARE TRANSACTION 'test_empty_transaction'; +ROLLBACK PREPARED 'test_empty_transaction'; +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); + data +------ +(0 rows) + -- Test 8: -- cleanup and make sure results are also empty DROP TABLE test_prepared1; diff --git a/contrib/test_decoding/sql/twophase.sql b/contrib/test_decoding/sql/twophase.sql index 4b9ef0c0c44..834e5282c30 100644 --- a/contrib/test_decoding/sql/twophase.sql +++ b/contrib/test_decoding/sql/twophase.sql @@ -125,6 +125,18 @@ COMMIT PREPARED 'test_toast_table_access'; -- consume commit prepared SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1', 'stream-changes', '1'); +-- Test that an empty prepared transaction should not be decoded, whether it +-- is committed or rolled back. +BEGIN; +SELECT * FROM test_prepared1 WHERE id = 1 FOR SHARE; +PREPARE TRANSACTION 'test_empty_transaction'; +COMMIT PREPARED 'test_empty_transaction'; +BEGIN; +SELECT * FROM test_prepared1 WHERE id = 1 FOR SHARE; +PREPARE TRANSACTION 'test_empty_transaction'; +ROLLBACK PREPARED 'test_empty_transaction'; +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); + -- Test 8: -- cleanup and make sure results are also empty DROP TABLE test_prepared1; diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c index ead803171e8..0330c6cea8f 100644 --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -2971,11 +2971,18 @@ ReorderBufferPrepare(ReorderBuffer *rb, TransactionId xid, txn->xact_time.prepare_time, txn->origin_id, txn->origin_lsn); /* - * Send a prepare if not already done so. This might occur if we have - * detected a concurrent abort while replaying the non-streaming - * transaction. + * Send a prepare if not already done so. The "not already sent" case can + * occur if we have detected a concurrent abort while replaying the + * non-streaming transaction; we still send the prepare so that later when + * rollback prepared is decoded and sent, the downstream should be able to + * rollback such a xact. See comments atop DecodePrepare. + * + * Skip this for a transaction that made no changes to the database (i.e. + * has no base snapshot), as we haven't sent any changes for it. Such a + * transaction is cleaned up without invoking the commit/rollback prepared + * callbacks in ReorderBufferFinishPrepared(). */ - if (!rbtxn_sent_prepare(txn)) + if (!rbtxn_sent_prepare(txn) && txn->base_snapshot != NULL) { rb->prepare(rb, txn, txn->final_lsn); txn->txn_flags |= RBTXN_SENT_PREPARE; @@ -3042,6 +3049,25 @@ ReorderBufferFinishPrepared(ReorderBuffer *rb, TransactionId xid, txn->xact_time.prepare_time, txn->origin_id, txn->origin_lsn); } + /* + * If this transaction has no snapshot, it didn't make any changes to the + * database, so there's nothing to decode. Note that + * ReorderBufferCommitChild will have transferred any snapshots from + * subtransactions if there were any. + */ + if (txn->base_snapshot == NULL) + { + Assert(txn->ninvalidations == 0); + Assert(!rbtxn_sent_prepare(txn)); + + /* + * Removing this txn before a commit might result in the computation + * of an incorrect restart_lsn. See SnapBuildProcessRunningXacts. + */ + ReorderBufferCleanupTXN(rb, txn); + return; + } + txn->final_lsn = commit_lsn; txn->end_lsn = end_lsn; txn->xact_time.commit_time = commit_time; diff --git a/src/test/subscription/t/021_twophase.pl b/src/test/subscription/t/021_twophase.pl index b8e4242d1f1..ab2e0a7fa8d 100644 --- a/src/test/subscription/t/021_twophase.pl +++ b/src/test/subscription/t/021_twophase.pl @@ -309,6 +309,47 @@ "SELECT count(*) FROM pg_prepared_xacts;"); is($result, qq(0), 'transaction is aborted on subscriber'); +############################### +# Test that an empty prepared transaction is not replicated. +# +# A transaction that is assigned an XID but makes no change decoded by logical +# replication (here, via a row lock) must not be sent to the subscriber. +# Otherwise the subscriber would receive a PREPARE with no preceding BEGIN +# PREPARE and error out, breaking replication. +############################### + +# An empty prepared transaction that is committed. +$node_publisher->safe_psql( + 'postgres', " + BEGIN; + SELECT a FROM tab_full WHERE a = 1 FOR SHARE; + PREPARE TRANSACTION 'test_empty_prepared'; + COMMIT PREPARED 'test_empty_prepared';"); + +# An empty prepared transaction that is rolled back. +$node_publisher->safe_psql( + 'postgres', " + BEGIN; + SELECT a FROM tab_full WHERE a = 1 FOR SHARE; + PREPARE TRANSACTION 'test_empty_prepared'; + ROLLBACK PREPARED 'test_empty_prepared';"); + +# A subsequent normal change must still replicate. Reaching catchup confirms +# the apply worker was not stalled by the empty prepared transactions above. +$node_publisher->safe_psql('postgres', "INSERT INTO tab_full VALUES (31);"); +$node_publisher->wait_for_catchup($appname); + +# The empty transactions must not have been prepared on the subscriber. +$result = $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM pg_prepared_xacts;"); +is($result, qq(0), 'empty prepared transaction is not replicated'); + +# The subsequent change is visible, so replication is healthy. +$result = $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM tab_full WHERE a = 31;"); +is($result, qq(1), + 'replication continues after an empty prepared transaction'); + ############################### # copy_data=false and two_phase ############################### From 277122036c3382c5ab47034a180fde1176728c43 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 28 Jul 2026 16:08:46 -0400 Subject: [PATCH 169/250] Fix planner's nullability/strictness logic for ScalarArrayOpExpr. find_nonnullable_rels and find_nonnullable_vars mistakenly treated a ScalarArrayOpExpr that could return FALSE as strict, but that's okay only at top level of a qual expression; further down, we've got to insist on a guaranteed-NULL result. The result was that we could draw mistaken conclusions about whether outer joins can be simplified, if the decision hinged on a non-top-level ScalarArrayOpExpr with a potentially-empty array argument. I believe this error dates to commit 72a070a36, which taught find_nonnullable_rels to descend into non-top-level parts of qual expressions. is_strict_saop (added earlier by 72153c058) already had enough intelligence to do the case correctly, but it wasn't passed the proper flag, ie "top_level" needs to be passed for "falseOK". e006a24ad copied that mistake into find_nonnullable_vars. Later, over-eager refactoring in commit 2f153ddfd broke contain_nonstrict_functions' handling of ScalarArrayOpExpr by treating it as though it were no different from an OpExpr. It is, because we must also prove the array is non-empty before concluding that the expression is strict. This could result in misclassifying an expression as strict when it is not, leading to assorted planning mistakes such as inlining a SQL function that shouldn't be inlined. We can almost fix this by just re-adding the previous handling of ScalarArrayOpExpr in that function, but doing only that would lead to also calling check_functions_in_node() and thus redundantly checking the operator's strictness. Avoid that by turning the if-series into an else-if chain, as it arguably should have been all along. The reason these errors have escaped detection for decades is that they are exposed only in arcane corner cases. ScalarArrayOpExpr with an empty array isn't typical usage, and even when that's possible several other conditions apply before the planner can reach a mistaken conclusion. While it's possible to build test cases demonstrating these mistakes, I (tgl) judged them too indirect and special-purpose to justify consuming regression test cycles forevermore. Author: Ayush Tiwari Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAJTYsWV3vqRJmST-gv1NsXEef-zOnjVJpYS910aBaiuMij4nFg@mail.gmail.com Discussion: https://postgr.es/m/CAJTYsWWcLGmz0f8_QPP_Liq-fc7-geiFSCdqoq3XGeRHPPsWeA@mail.gmail.com Backpatch-through: 14 --- src/backend/optimizer/util/clauses.c | 74 +++++++++++++++------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 1d341db7d06..aa70667c4a5 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -1025,7 +1025,7 @@ contain_nonstrict_functions_walker(Node *node, void *context) /* an aggregate could return non-null with null input */ return true; } - if (IsA(node, GroupingFunc)) + else if (IsA(node, GroupingFunc)) { /* * A GroupingFunc doesn't evaluate its arguments, and therefore must @@ -1033,12 +1033,12 @@ contain_nonstrict_functions_walker(Node *node, void *context) */ return true; } - if (IsA(node, WindowFunc)) + else if (IsA(node, WindowFunc)) { /* a window function could return non-null with null input */ return true; } - if (IsA(node, SubscriptingRef)) + else if (IsA(node, SubscriptingRef)) { SubscriptingRef *sbsref = (SubscriptingRef *) node; const SubscriptRoutines *sbsroutines; @@ -1052,17 +1052,25 @@ contain_nonstrict_functions_walker(Node *node, void *context) return true; /* else fall through to check args */ } - if (IsA(node, DistinctExpr)) + else if (IsA(node, DistinctExpr)) { /* IS DISTINCT FROM is inherently non-strict */ return true; } - if (IsA(node, NullIfExpr)) + else if (IsA(node, NullIfExpr)) { /* NULLIF is inherently non-strict */ return true; } - if (IsA(node, BoolExpr)) + else if (IsA(node, ScalarArrayOpExpr)) + { + ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node; + + if (!is_strict_saop(expr, false)) + return true; + /* else fall through to check args */ + } + else if (IsA(node, BoolExpr)) { BoolExpr *expr = (BoolExpr *) node; @@ -1076,28 +1084,26 @@ contain_nonstrict_functions_walker(Node *node, void *context) break; } } - if (IsA(node, SubLink)) + else if (IsA(node, SubLink)) { /* In some cases a sublink might be strict, but in general not */ return true; } - if (IsA(node, SubPlan)) + else if (IsA(node, SubPlan)) return true; - if (IsA(node, AlternativeSubPlan)) + else if (IsA(node, AlternativeSubPlan)) return true; - if (IsA(node, FieldStore)) + else if (IsA(node, FieldStore)) return true; - if (IsA(node, CoerceViaIO)) + else if (IsA(node, CoerceViaIO)) { /* * CoerceViaIO is strict regardless of whether the I/O functions are, - * so just go look at its argument; asking check_functions_in_node is - * useless expense and could deliver the wrong answer. + * so we should skip check_functions_in_node() and just fall through + * to check the arguments. */ - return contain_nonstrict_functions_walker((Node *) ((CoerceViaIO *) node)->arg, - context); } - if (IsA(node, ArrayCoerceExpr)) + else if (IsA(node, ArrayCoerceExpr)) { /* * ArrayCoerceExpr is strict at the array level, regardless of what @@ -1107,31 +1113,33 @@ contain_nonstrict_functions_walker(Node *node, void *context) return contain_nonstrict_functions_walker((Node *) ((ArrayCoerceExpr *) node)->arg, context); } - if (IsA(node, CaseExpr)) + else if (IsA(node, CaseExpr)) return true; - if (IsA(node, ArrayExpr)) + else if (IsA(node, ArrayExpr)) return true; - if (IsA(node, RowExpr)) + else if (IsA(node, RowExpr)) return true; - if (IsA(node, RowCompareExpr)) + else if (IsA(node, RowCompareExpr)) return true; - if (IsA(node, CoalesceExpr)) + else if (IsA(node, CoalesceExpr)) return true; - if (IsA(node, MinMaxExpr)) + else if (IsA(node, MinMaxExpr)) return true; - if (IsA(node, XmlExpr)) + else if (IsA(node, XmlExpr)) return true; - if (IsA(node, NullTest)) - return true; - if (IsA(node, BooleanTest)) + else if (IsA(node, NullTest)) return true; - if (IsA(node, JsonConstructorExpr)) + else if (IsA(node, BooleanTest)) return true; - - /* Check other function-containing nodes */ - if (check_functions_in_node(node, contain_nonstrict_functions_checker, - context)) + else if (IsA(node, JsonConstructorExpr)) return true; + else + { + /* Check other function-containing nodes */ + if (check_functions_in_node(node, contain_nonstrict_functions_checker, + context)) + return true; + } return expression_tree_walker(node, contain_nonstrict_functions_walker, context); @@ -1527,7 +1535,7 @@ find_nonnullable_rels_walker(Node *node, bool top_level) { ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node; - if (is_strict_saop(expr, true)) + if (is_strict_saop(expr, top_level)) result = find_nonnullable_rels_walker((Node *) expr->args, false); } else if (IsA(node, BoolExpr)) @@ -1780,7 +1788,7 @@ find_nonnullable_vars_walker(Node *node, bool top_level) { ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node; - if (is_strict_saop(expr, true)) + if (is_strict_saop(expr, top_level)) result = find_nonnullable_vars_walker((Node *) expr->args, false); } else if (IsA(node, BoolExpr)) From 33101632235ad064b2bd7bc04a5066048dc48023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Wed, 29 Jul 2026 17:15:45 +0200 Subject: [PATCH 170/250] Fix cascading standby reconnect failure after archive fallback A cascading standby could fail to reconnect to its upstream standby with "requested starting point ... is ahead of the WAL flush position" after falling back to archive recovery. This happened because archive recovery processes whole segment files, so after replaying a segment the cascade's next read position lands at the start of the following segment, which is ahead of the upstream's flush position reported by GetStandbyFlushRecPtr() (still inside the just-replayed segment). Fix by having the walreceiver check the upstream's current WAL flush position via IDENTIFY_SYSTEM before issuing START_REPLICATION. IDENTIFY_SYSTEM already returns this position (as xlogpos), but walrcv_identify_system() previously discarded it; now we have a use for it. If the requested start point exceeds the upstream's flush position on the same timeline, the walreceiver waits for wal_retrieve_retry_interval and retries. The wait is limited to gaps of at most one WAL segment, which is the expected case from the segment-granularity of archive recovery. Larger gaps indicate the upstream is genuinely behind, so START_REPLICATION is allowed to proceed (and fail) normally, letting the startup process fall back to other WAL sources. The first wait is logged at LOG level; subsequent waits are demoted to DEBUG1 to avoid log noise. The walreceiver honors wal_receiver_timeout during the wait, so it will exit if the upstream doesn't catch up in time. To preserve ABI compatibility on back branches, the flush position from IDENTIFY_SYSTEM is communicated via a new global variable (WalRcvIdentifySystemLsn) rather than changing the signature of walrcv_identify_system(). The bug was introduced in Postgres 9.3 by commit abfd192b1b5b, which added a flush-position check in StartReplication() that rejects requests ahead of the upstream server's WAL flush position. Author: Marco Nenciarini Reviewed-by: Xuneng Zhou Backpatch-through: 14 Discussion: https://postgr.es/m/CA+nrD2cTuTkkX5WXVZengTYYZbAO6zV8K+Tri-R0fbLFuoyMBA@mail.gmail.com --- .../libpqwalreceiver/libpqwalreceiver.c | 14 ++ src/backend/replication/walreceiver.c | 79 +++++++++- .../utils/activity/wait_event_names.txt | 1 + src/include/replication/walreceiver.h | 7 + src/test/recovery/meson.build | 1 + src/test/recovery/t/055_cascade_reconnect.pl | 148 ++++++++++++++++++ 6 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 src/test/recovery/t/055_cascade_reconnect.pl diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c index b0a081dd881..ec41ce87a70 100644 --- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c +++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c @@ -458,6 +458,20 @@ libpqrcv_identify_system(WalReceiverConn *conn, TimeLineID *primary_tli) } primary_sysid = pstrdup(PQgetvalue(res, 0, 0)); *primary_tli = pg_strtoint32(PQgetvalue(res, 0, 1)); + + /* Column 2 is the server's current WAL flush position */ + { + uint32 hi, + lo; + + if (sscanf(PQgetvalue(res, 0, 2), "%X/%X", &hi, &lo) != 2) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not parse WAL location \"%s\"", + PQgetvalue(res, 0, 2)))); + WalRcvIdentifySystemLsn = ((uint64) hi) << 32 | lo; + } + PQclear(res); return primary_sysid; diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c index 6df19f89bef..49bf48a3064 100644 --- a/src/backend/replication/walreceiver.c +++ b/src/backend/replication/walreceiver.c @@ -54,6 +54,7 @@ #include "access/htup_details.h" #include "access/timeline.h" #include "access/transam.h" +#include "access/xlog.h" #include "access/xlog_internal.h" #include "access/xlogarchive.h" #include "access/xlogrecovery.h" @@ -93,6 +94,12 @@ bool hot_standby_feedback; static WalReceiverConn *wrconn = NULL; WalReceiverFunctionsType *WalReceiverFunctions = NULL; +/* + * Server's WAL flush position from the last IDENTIFY_SYSTEM call. + * Written by libpqwalreceiver, read by walreceiver main loop. + */ +XLogRecPtr WalRcvIdentifySystemLsn = InvalidXLogRecPtr; + /* * These variables are used similarly to openLogFile/SegNo, * but for walreceiver to write the XLOG. recvFileTLI is the TimeLineID @@ -159,6 +166,8 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) TimeLineID startpointTLI; TimeLineID primaryTLI; bool first_stream; + bool upstream_catchup_logged = false; + TimestampTz upstream_catchup_deadline = 0; WalRcvData *walrcv; TimestampTz now; char *err; @@ -311,8 +320,10 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) /* * Check that we're connected to a valid server using the - * IDENTIFY_SYSTEM replication command. + * IDENTIFY_SYSTEM replication command. Reset the global LSN + * first so we don't act on a stale value if the call fails. */ + WalRcvIdentifySystemLsn = InvalidXLogRecPtr; primary_sysid = walrcv_identify_system(wrconn, &primaryTLI); snprintf(standby_sysid, sizeof(standby_sysid), UINT64_FORMAT, @@ -336,6 +347,72 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) errmsg("highest timeline %u of the primary is behind recovery timeline %u", primaryTLI, startpointTLI))); + /* + * If our requested startpoint is ahead of the upstream server's + * current WAL flush position, we cannot start streaming yet. (We say + * "upstream" here and not "primary" because this condition can only + * happen on a cascading standby.) This can happen when such a + * cascading standby has advanced past the upstream via archive + * recovery but the intermediate standby has not caught up with that + * yet. In this case, wait for the upstream to catch up before + * attempting START_REPLICATION, because that would fail with + * "requested starting point is ahead of the WAL flush position". + * + * We only perform this check when we're on the same timeline as the + * primary; when timelines differ, let START_REPLICATION handle the + * timeline negotiation. + * + * We also only wait if the gap is within one WAL segment. This is + * the expected case because archive recovery processes whole segment + * files: the cascade's next read position lands at the start of the + * following segment while the upstream's flush position is still + * inside the just-replayed one, producing at most a sub-segment gap. + * A larger gap means the upstream is genuinely behind, so we let + * START_REPLICATION fail normally and allow the startup process to + * fall back to other WAL sources. + * + * Honor wal_receiver_timeout so the walreceiver doesn't wait + * indefinitely: if the upstream hasn't caught up within the timeout, + * exit and let the startup process retry normally. + */ + if (startpointTLI == primaryTLI && + XLogRecPtrIsValid(WalRcvIdentifySystemLsn) && + startpoint > WalRcvIdentifySystemLsn && + startpoint - WalRcvIdentifySystemLsn <= wal_segment_size) + { + /* Set deadline on first iteration */ + if (!upstream_catchup_logged && wal_receiver_timeout > 0) + upstream_catchup_deadline = + TimestampTzPlusMilliseconds(GetCurrentTimestamp(), + wal_receiver_timeout); + + ereport(upstream_catchup_logged ? DEBUG1 : LOG, + errmsg("walreceiver requested start point %X/%08X on timeline %u is ahead of the upstream server's flush position %X/%08X, waiting", + LSN_FORMAT_ARGS(startpoint), startpointTLI, + LSN_FORMAT_ARGS(WalRcvIdentifySystemLsn))); + upstream_catchup_logged = true; + + (void) WaitLatch(MyLatch, + WL_EXIT_ON_PM_DEATH | WL_TIMEOUT | WL_LATCH_SET, + wal_retrieve_retry_interval, + WAIT_EVENT_WAL_RECEIVER_UPSTREAM_CATCHUP); + ResetLatch(MyLatch); + + if (upstream_catchup_deadline > 0 && + GetCurrentTimestamp() >= upstream_catchup_deadline) + ereport(ERROR, + (errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("terminating walreceiver due to timeout while waiting for upstream to catch up"))); + + CHECK_FOR_INTERRUPTS(); + continue; + } + else + { + upstream_catchup_logged = false; + upstream_catchup_deadline = 0; + } + /* * Get any missing history files. We do this always, even when we're * not interested in that timeline, so that if we're promoted to diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index b9c1e6900ec..6f15bd6978c 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -162,6 +162,7 @@ WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." ABI_compatibility: +WAL_RECEIVER_UPSTREAM_CATCHUP "Waiting for upstream server WAL flush position to catch up to requested start point." # # Wait Events - Timeout diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h index 89f63f908f8..8d0354e1003 100644 --- a/src/include/replication/walreceiver.h +++ b/src/include/replication/walreceiver.h @@ -164,6 +164,13 @@ typedef struct extern PGDLLIMPORT WalRcvData *WalRcv; +/* + * Server's WAL flush position as reported by the last IDENTIFY_SYSTEM call. + * Set by walrcv_identify_system(), used by the walreceiver to avoid + * requesting streaming from a point ahead of the upstream's flush position. + */ +extern PGDLLIMPORT XLogRecPtr WalRcvIdentifySystemLsn; + typedef struct { bool logical; /* True if this is logical replication stream, diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build index 38e1e43e041..3c7d0492bba 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -59,6 +59,7 @@ tests += { 't/048_vacuum_horizon_floor.pl', 't/053_standby_login_event_trigger.pl', 't/054_unlogged_sequence_promotion.pl', + 't/055_cascade_reconnect.pl', ], }, } diff --git a/src/test/recovery/t/055_cascade_reconnect.pl b/src/test/recovery/t/055_cascade_reconnect.pl new file mode 100644 index 00000000000..051a3292533 --- /dev/null +++ b/src/test/recovery/t/055_cascade_reconnect.pl @@ -0,0 +1,148 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test that a cascading standby can reconnect to its upstream standby after +# advancing past the upstream's WAL flush position via archive recovery. +# +# Setup: praline -> samurai -> stubble +# stubble has both streaming (from samurai) and restore_command +# (from praline's archive). +# +# When samurai's walreceiver is stopped and stubble falls back to +# archive recovery, stubble may advance its recovery position past +# samurai's replay position. Previously, stubble's walreceiver +# would fail with "requested starting point is ahead of the WAL flush +# position" when reconnecting to samurai. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize praline with archiving +my $praline = PostgreSQL::Test::Cluster->new('praline'); +$praline->init(allows_streaming => 1, has_archiving => 1); +$praline->append_conf( + 'postgresql.conf', qq( +wal_keep_size = 128MB +checkpoint_timeout = 1h +)); +$praline->start; + +# Take backup and create samurai (streaming from praline, no archive) +my $backup_name = 'my_backup'; +$praline->backup($backup_name); + +my $samurai = PostgreSQL::Test::Cluster->new('samurai'); +$samurai->init_from_backup($praline, $backup_name, has_streaming => 1); +$samurai->start; + +# Wait for samurai to start streaming +$praline->wait_for_catchup($samurai); + +# Take backup from samurai and create stubble +# stubble streams from samurai AND restores from praline's archive +$samurai->backup($backup_name); + +my $stubble = PostgreSQL::Test::Cluster->new('stubble'); +$stubble->init_from_backup($samurai, $backup_name, has_streaming => 1); +$stubble->enable_restoring($praline); +$stubble->start; + +# Generate initial data and wait for full cascade replication +$praline->safe_psql('postgres', + "CREATE TABLE test_tab AS SELECT generate_series(1, 1000) AS id"); +$praline->wait_for_replay_catchup($samurai); +$samurai->wait_for_replay_catchup($stubble, $praline); + +my $result = $stubble->safe_psql('postgres', "SELECT count(*) FROM test_tab"); +is($result, '1000', 'initial data replicated to stubble'); + +# Disconnect samurai from praline by clearing primary_conninfo. +# This stops samurai's walreceiver, so samurai can no longer receive +# new WAL. Its GetStandbyFlushRecPtr() will return only replayPtr. +$samurai->append_conf('postgresql.conf', "primary_conninfo = ''"); +$samurai->reload; + +# Wait for samurai's walreceiver to stop +$samurai->poll_query_until('postgres', + "SELECT NOT EXISTS (SELECT 1 FROM pg_stat_wal_receiver)") + or die "Timed out waiting for samurai walreceiver to stop"; + +# Stop stubble cleanly. We'll restart it after generating new WAL +# so it enters the recovery state machine fresh and tries archive first. +$stubble->stop; + +# Force a checkpoint now so that no background checkpoint can generate +# extra WAL during the INSERT below and push it across a segment boundary. +# Combined with checkpoint_timeout = 1h this ensures the new WAL fits +# within a single segment, keeping the gap within wal_segment_size. +$praline->safe_psql('postgres', "CHECKPOINT"); + +# Generate more WAL on praline +$praline->safe_psql('postgres', + "INSERT INTO test_tab SELECT generate_series(1001, 2000)"); + +# Force WAL switch and wait for archiving to complete, so that +# stubble can find the new WAL in the archive when it starts. +my $walfile = $praline->safe_psql('postgres', + "SELECT pg_walfile_name(pg_current_wal_lsn())"); +$praline->safe_psql('postgres', "SELECT pg_switch_wal()"); +$praline->poll_query_until('postgres', + "SELECT '$walfile' <= last_archived_wal FROM pg_stat_archiver") + or die "Timed out waiting for WAL archiving"; + +# Rotate stubble's log so we can check just the new log output +$stubble->rotate_logfile; +my $stubble_log_offset = -s $stubble->logfile; + +# Start stubble. It will: +# 1. Read new WAL from praline's archive (XLOG_FROM_ARCHIVE) +# 2. Advance RecPtr past samurai's replay position +# 3. Try streaming from samurai (XLOG_FROM_STREAM) +# 4. detect that upstream is behind via +# IDENTIFY_SYSTEM and wait instead of failing +$stubble->start; + +# Wait for stubble to replay the new data from archive +$stubble->poll_query_until('postgres', + "SELECT count(*) >= 2000 FROM test_tab") + or die "Timed out waiting for stubble to replay archived WAL"; + +$result = $stubble->safe_psql('postgres', "SELECT count(*) FROM test_tab"); +is($result, '2000', 'stubble replayed new data from archive'); + +# Wait for walreceiver to hit the upstream-catchup wait event, proving we +# exercised the START_REPLICATION-ahead-of-upstream path. +$stubble->wait_for_event('walreceiver', 'WalReceiverUpstreamCatchup'); + +# Verify no errors occurred in stubble. +my $stubble_loglines = + PostgreSQL::Test::Utils::slurp_file($stubble->logfile, $stubble_log_offset); +ok( $stubble_loglines !~ m/ERROR/, 'no errors in stubble log'); + +# Now restore samurai's streaming from praline so it can catch up +$samurai->enable_streaming($praline); +$samurai->reload; + +# Wait for samurai to catch up with praline +$praline->wait_for_replay_catchup($samurai); + +# stubble's walreceiver should eventually connect to samurai and +# resume streaming (once samurai has caught up past stubble's position) +$samurai->poll_query_until('postgres', + "SELECT EXISTS (SELECT 1 FROM pg_stat_replication)") + or die "Timed out waiting for stubble to reconnect to samurai"; + +# Verify end-to-end cascade streaming works with new data +$praline->safe_psql('postgres', + "INSERT INTO test_tab SELECT generate_series(2001, 3000)"); +$praline->wait_for_replay_catchup($samurai); +$samurai->wait_for_replay_catchup($stubble, $praline); + +$result = $stubble->safe_psql('postgres', "SELECT count(*) FROM test_tab"); +is($result, '3000', + 'cascade streaming resumes normally after upstream catches up'); + +done_testing(); From 050e9d94d81bb1f6f5b34cb7794f201611fc9afa Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Wed, 29 Jul 2026 21:51:46 +0200 Subject: [PATCH 171/250] doc: Add a note that refint will be removed in v20 refint has been removed from the spi contrib module in v20. Add a note to the documentation of the still-supported back branches so that users are aware the module is going away. Author: Ayush Tiwari Reported-by: Daniel Gustafsson Discussion: https://postgr.es/m/CAJTYsWUHq8Ohc6-N-xamOPYz-q3qUYMtwQX-1=Zi=5N1Q_GSEQ@mail.gmail.com Backpatch-through: 14 --- doc/src/sgml/contrib-spi.sgml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/contrib-spi.sgml b/doc/src/sgml/contrib-spi.sgml index 7e4e580bc74..9f397aeaddf 100644 --- a/doc/src/sgml/contrib-spi.sgml +++ b/doc/src/sgml/contrib-spi.sgml @@ -31,7 +31,8 @@ check_primary_key() and check_foreign_key() are used to check foreign key constraints. (This functionality is long since superseded by the built-in foreign - key mechanism, of course, but the module is still useful as an example.) + key mechanism, of course, but the module is still useful as an example. + This module will be removed in PostgreSQL 20.) From 0abdb3e3610e171993e2951523a2d11284c23b7d Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Thu, 30 Jul 2026 11:25:12 +0530 Subject: [PATCH 172/250] Skip SUBSCRIPTION TABLE TOC entries with --no-subscriptions. pg_dump in --binary-upgrade mode emits "SUBSCRIPTION TABLE" TOC entries to preserve pg_subscription_rel state across pg_upgrade. When such a dump was restored with --no-subscriptions, _tocEntryRequired() skipped the "SUBSCRIPTION" entry but not the associated "SUBSCRIPTION TABLE" entries, so the restore would try to apply subscription-relation state for a subscription that was never created. Skip "SUBSCRIPTION TABLE" entries as well when no_subscriptions is set. This can happen when pg_subscription_rel has entries, the dump is taken with --binary-upgrade, and it is restored with --no-subscriptions. Reported-by: Hayato Kuroda Author: Hayato Kuroda Reviewed-by: Shlok Kyal Reviewed-by: Amit Kapila Backpatch-through: 17, where it was introduced Discussion: https://postgr.es/m/OS9PR01MB121493DA4C1A7748B11A646D8F5C02@OS9PR01MB12149.jpnprd01.prod.outlook.com --- src/bin/pg_dump/pg_backup_archiver.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 8f35bbb3779..efcb5924935 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3083,7 +3083,9 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) } /* If it's a subscription, maybe ignore it */ - if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0) + if (ropt->no_subscriptions && + (strcmp(te->desc, "SUBSCRIPTION") == 0 || + strcmp(te->desc, "SUBSCRIPTION TABLE") == 0)) return 0; /* Ignore it if section is not to be dumped/restored */ From d4420a97206cad5bcf63405062db5b4ebaa7f2b1 Mon Sep 17 00:00:00 2001 From: Tomas Vondra Date: Thu, 30 Jul 2026 14:06:20 +0200 Subject: [PATCH 173/250] Initialize bs_reltuples in parallel GIN builds Index builds update pg_class.reltuples for the table. In parallel GIN builds, workers track the number of processed rows, and report it to the leader, who then updates the pg_class with a total. However, gin_parallel_build_main failed to initialize the bs_reltuples field, leaving it set to whatever happens to be on the stack (which may be bogus values like Infinity or NaN, or just impossibly high values). If such values get reported to the leader and stored in pg_class, that can have serious consequences. The pg_class.reltuples field is used to decide when a table is due for autovacuum or autoanalyze, and if it happens to be set to a bogus value, that may never happen. The field is also used by the optimizer when calculating costs. Fixed by initializing bs_reltuples together with the rest of the build state. The bs_numtuples was initialized later, but it seems cleaner to just initialize all the fields at once. After a bogus value gets persisted in pg_class, affected systems are unlikely to self-heal. That would require an ANALYZE, but preventing that is one of the consequences. We have considered forcing autoanalyze in these cases, but there's not a good way to reliably identify bogus values (except for a small minority like Infitiny/NaN). A manual ANALYZE on (possibly) affected tables is the only solution. Backpatch to 18, where parallel GIN builds were introduced. Reported-by: Jan Nidzwetzki Discussion: https://postgr.es/m/518BA772-8026-412A-AA8F-A7FE4C6B3717@planetscale.com Backpatch-through: 18 --- src/backend/access/gin/gininsert.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c index a72e531167b..a575afacab3 100644 --- a/src/backend/access/gin/gininsert.c +++ b/src/backend/access/gin/gininsert.c @@ -1867,9 +1867,6 @@ _gin_process_worker_data(GinBuildState *state, Tuplesortstate *worker_sort, tuplesort_performsort(state->bs_worker_sort); - /* reset the number of GIN tuples produced by this worker */ - state->bs_numtuples = 0; - if (progress) pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_GIN_PHASE_MERGE_1); @@ -2149,6 +2146,11 @@ _gin_parallel_build_main(dsm_segment *seg, shm_toc *toc) /* initialize the GIN build state */ initGinState(&buildstate.ginstate, indexRel); buildstate.indtuples = 0; + + /* Initialize counters used to report tuple counts to the leader */ + buildstate.bs_numtuples = 0; + buildstate.bs_reltuples = 0; + memset(&buildstate.buildStats, 0, sizeof(GinStatsData)); memset(&buildstate.tid, 0, sizeof(ItemPointerData)); From a0369dd8448fba99d22068f533764bf2fb001278 Mon Sep 17 00:00:00 2001 From: Tomas Vondra Date: Thu, 30 Jul 2026 15:30:19 +0200 Subject: [PATCH 174/250] Reject non-finite reltuples when restoring stats When restoring relation stats, pg_restore_relation_stats() rejected calls with (reltuples < -1.0). But that is insufficient - Infinity and NaN values both pass that check, and get stored in pg_class verbatim. This can have various undesirable consequences. Fixed by rejecting non-finite reltuple values, in the same non-fatal way as for the existing checks (emit WARNING and skip the update). Adds a regression test to stats_import for these non-finite values, and to check the -1.0 special value is still accepted. Backpatch to 18, where pg_restore_relation_stats() was introduced. Patch by Jan Nidzwetzki, minor commit message tweaks by me. Author: Jan Nidzwetzki Discussion: https://postgr.es/m/518BA772-8026-412A-AA8F-A7FE4C6B3717@planetscale.com Backpatch-through: 18 --- src/backend/statistics/relation_stats.c | 11 ++- src/test/regress/expected/stats_import.out | 79 ++++++++++++++++++++++ src/test/regress/sql/stats_import.sql | 43 ++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c index 174da7d93a5..4b85207260b 100644 --- a/src/backend/statistics/relation_stats.c +++ b/src/backend/statistics/relation_stats.c @@ -17,6 +17,8 @@ #include "postgres.h" +#include + #include "access/heapam.h" #include "catalog/indexing.h" #include "catalog/namespace.h" @@ -110,7 +112,14 @@ relation_statistics_update(FunctionCallInfo fcinfo) if (!PG_ARGISNULL(RELTUPLES_ARG)) { reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG); - if (reltuples < -1.0) + if (isnan(reltuples) || isinf(reltuples)) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("argument \"%s\" must be a finite value", "reltuples"))); + result = false; + } + else if (reltuples < -1.0) { ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out index 2efd422df16..4d3080f7a56 100644 --- a/src/test/regress/expected/stats_import.out +++ b/src/test/regress/expected/stats_import.out @@ -241,6 +241,85 @@ WHERE oid = 'stats_import.test'::regclass; 16 | 500 | 4 | 2 (1 row) +-- error: reltuples must be finite (rejected with WARNING, returns false) +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', 'Infinity'::real); +WARNING: argument "reltuples" must be a finite value + pg_restore_relation_stats +--------------------------- + f +(1 row) + +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', '-Infinity'::real); +WARNING: argument "reltuples" must be a finite value + pg_restore_relation_stats +--------------------------- + f +(1 row) + +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', 'NaN'::real); +WARNING: argument "reltuples" must be a finite value + pg_restore_relation_stats +--------------------------- + f +(1 row) + +-- error: reltuples must not be less than -1.0 (rejected with WARNING, returns false) +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', '-5'::real); +WARNING: argument "reltuples" must not be less than -1.0 + pg_restore_relation_stats +--------------------------- + f +(1 row) + +-- reltuples is unchanged (still 500) after the rejected values above +SELECT relpages, reltuples, relallvisible, relallfrozen +FROM pg_class +WHERE oid = 'stats_import.test'::regclass; + relpages | reltuples | relallvisible | relallfrozen +----------+-----------+---------------+-------------- + 16 | 500 | 4 | 2 +(1 row) + +-- ok: -1 (the "unknown" sentinel) is still accepted +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', '-1'::real); + pg_restore_relation_stats +--------------------------- + t +(1 row) + +SELECT relpages, reltuples, relallvisible, relallfrozen +FROM pg_class +WHERE oid = 'stats_import.test'::regclass; + relpages | reltuples | relallvisible | relallfrozen +----------+-----------+---------------+-------------- + 16 | -1 | 4 | 2 +(1 row) + +-- restore reltuples to 500 for the following tests +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', '500'::real); + pg_restore_relation_stats +--------------------------- + t +(1 row) + -- ok: set just relallvisible, rest stay same SELECT pg_restore_relation_stats( 'schemaname', 'stats_import', diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index ee97fa6bc1b..e53049bdf87 100644 --- a/src/test/regress/sql/stats_import.sql +++ b/src/test/regress/sql/stats_import.sql @@ -177,6 +177,49 @@ SELECT relpages, reltuples, relallvisible, relallfrozen FROM pg_class WHERE oid = 'stats_import.test'::regclass; +-- error: reltuples must be finite (rejected with WARNING, returns false) +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', 'Infinity'::real); + +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', '-Infinity'::real); + +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', 'NaN'::real); + +-- error: reltuples must not be less than -1.0 (rejected with WARNING, returns false) +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', '-5'::real); + +-- reltuples is unchanged (still 500) after the rejected values above +SELECT relpages, reltuples, relallvisible, relallfrozen +FROM pg_class +WHERE oid = 'stats_import.test'::regclass; + +-- ok: -1 (the "unknown" sentinel) is still accepted +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', '-1'::real); + +SELECT relpages, reltuples, relallvisible, relallfrozen +FROM pg_class +WHERE oid = 'stats_import.test'::regclass; + +-- restore reltuples to 500 for the following tests +SELECT pg_restore_relation_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'reltuples', '500'::real); + -- ok: set just relallvisible, rest stay same SELECT pg_restore_relation_stats( 'schemaname', 'stats_import', From 02e69be47c05c10c8ee8ac5d7634a4048c037ebc Mon Sep 17 00:00:00 2001 From: David Rowley Date: Fri, 31 Jul 2026 15:37:42 +1200 Subject: [PATCH 175/250] Fix issue with RANGE's DEFAULT partition pruning Partition pruning for RANGE-partitioned tables could mistakenly prune the DEFAULT partition in some cases when it was not valid to do so, which could lead to rows missing from query results. The only known cases where this could happen is when combining pruning steps from an IS NOT NULL clause with other steps that matched to the DEFAULT partition. This could occur due to RANGE partitioned tables having two distinct internal representations for marking if the DEFAULT partition should be scanned. The IS NOT NULL steps would mark the "scan_default" boolean, but other steps created for different purposes could mark a bound_offset Bitmapset, which would ultimately translate into also scanning the default partition. This could all fail after multiple steps were combined with a combine intersect operator, as that will intersect the bound_offset bits and only set scan_default if all pruning steps have that flag set. When both input steps to the intersect operator had different representations of whether to scan the DEFAULT partition, the resulting intersect step result would contain neither representation. Here, we fix this by having the IS NOT NULL pruning result mark the bound_offsets so that it uses both representations to mark that the DEFAULT partition must be scanned. Reported-by: Jacob Brazeal Diagnosed-by: Jacob Brazeal Author: David Rowley Discussion: https://postgr.es/m/CA+COZaDXrfTaBjLE=Z79MTaH6Xun1V4PeKxLvCNv8mXS8wn0rw@mail.gmail.com Backpatch-through: 14 --- src/backend/partitioning/partprune.c | 10 +--- src/test/regress/expected/partition_prune.out | 46 +++++++++++++++++++ src/test/regress/sql/partition_prune.sql | 20 ++++++++ 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c index 2ef98d6ad6e..5dae151da0f 100644 --- a/src/backend/partitioning/partprune.c +++ b/src/backend/partitioning/partprune.c @@ -3032,15 +3032,9 @@ get_matching_range_bounds(PartitionPruneContext *context, */ if (nvalues == 0) { - /* ignore key space not covered by any partitions */ - if (partindices[minoff] < 0) - minoff++; - if (partindices[maxoff] < 0) - maxoff--; - result->scan_default = partition_bound_has_default(boundinfo); - Assert(partindices[minoff] >= 0 && - partindices[maxoff] >= 0); + Assert(partindices[minoff] >= -1 && + partindices[maxoff] >= -1); result->bound_offsets = bms_add_range(NULL, minoff, maxoff); return result; diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out index 7aa3e5cbaf9..a107281daf7 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -689,6 +689,52 @@ explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = Filter: (((a = 1) AND (a = 3)) OR ((a > 1) AND (a = 15))) (11 rows) +-- Test cases for range partitioned tables with IN clauses. +create table rangepart (a int) partition by range (a); +create table rangepart1 partition of rangepart for values from (0) to (10); +create table rangepart2 partition of rangepart for values from (10) to (20); +create table rangepart_def partition of rangepart default; +-- Ensure we scan all apart from the default partition +explain (costs off) select * from rangepart where a in(5,15); + QUERY PLAN +------------------------------------------------- + Append + -> Seq Scan on rangepart1 rangepart_1 + Filter: (a = ANY ('{5,15}'::integer[])) + -> Seq Scan on rangepart2 rangepart_2 + Filter: (a = ANY ('{5,15}'::integer[])) +(5 rows) + +-- Ensure we scan only the default +explain (costs off) select * from rangepart where a in(20,21); + QUERY PLAN +-------------------------------------------- + Seq Scan on rangepart_def rangepart + Filter: (a = ANY ('{20,21}'::integer[])) +(2 rows) + +-- Ensure we scan only the default +explain (costs off) select * from rangepart where a in(-1,20); + QUERY PLAN +-------------------------------------------- + Seq Scan on rangepart_def rangepart + Filter: (a = ANY ('{-1,20}'::integer[])) +(2 rows) + +-- Ensure we scan all partitions +explain (costs off) select * from rangepart where a is not null and a in(-1,5,15,20); + QUERY PLAN +----------------------------------------------------------------------------- + Append + -> Seq Scan on rangepart1 rangepart_1 + Filter: ((a IS NOT NULL) AND (a = ANY ('{-1,5,15,20}'::integer[]))) + -> Seq Scan on rangepart2 rangepart_2 + Filter: ((a IS NOT NULL) AND (a = ANY ('{-1,5,15,20}'::integer[]))) + -> Seq Scan on rangepart_def rangepart_3 + Filter: ((a IS NOT NULL) AND (a = ANY ('{-1,5,15,20}'::integer[]))) +(7 rows) + +drop table rangepart; -- multi-column keys create table mc3p (a int, b int, c int) partition by range (a, abs(b), c); create table mc3p_default partition of mc3p default; diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql index 359a9208056..dac673ef80a 100644 --- a/src/test/regress/sql/partition_prune.sql +++ b/src/test/regress/sql/partition_prune.sql @@ -118,6 +118,26 @@ explain (costs off) select * from rlp where a > 1 and a >=15; /* rlp3 onwards, i explain (costs off) select * from rlp where a = 1 and a = 3; /* empty */ explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = 15); +-- Test cases for range partitioned tables with IN clauses. +create table rangepart (a int) partition by range (a); +create table rangepart1 partition of rangepart for values from (0) to (10); +create table rangepart2 partition of rangepart for values from (10) to (20); +create table rangepart_def partition of rangepart default; + +-- Ensure we scan all apart from the default partition +explain (costs off) select * from rangepart where a in(5,15); + +-- Ensure we scan only the default +explain (costs off) select * from rangepart where a in(20,21); + +-- Ensure we scan only the default +explain (costs off) select * from rangepart where a in(-1,20); + +-- Ensure we scan all partitions +explain (costs off) select * from rangepart where a is not null and a in(-1,5,15,20); + +drop table rangepart; + -- multi-column keys create table mc3p (a int, b int, c int) partition by range (a, abs(b), c); create table mc3p_default partition of mc3p default; From f70acc8a2b96e0b565836799b6ccc3bacba8068a Mon Sep 17 00:00:00 2001 From: David Rowley Date: Fri, 31 Jul 2026 23:24:46 +1200 Subject: [PATCH 176/250] Fix Hash Join performance issue when hashing NULL values adf97c156 allowed expression evaluation to perform hashing, and subsequently 9ca67658d fixed a memory stomping bug in that commit that caused unrelated-to-hashing expression op steps to stomp on the intermediate hash value. The intermediate hash value needs to be maintained when hashing multiple hash keys. 9ca67658d didn't quite get things right when in "strict" mode when it aborted hashing early after encountering a NULL hash key. What was meant to happen was that the expression returns NULL directly to indicate to the caller the value hashed to NULL. The problem was that any EEOP_HASHDATUM_FIRST_STRICT or EEOP_HASHDATUM_NEXT32_STRICT op step that didn't belong to the final key to be hashed would have its op->resnull and op->resvalue pointing to the location to store the intermediate hash value. That's correct for non-NULLs since we bit-rotate the intermediate value and continue hashing, but with the strict case, when we get a NULL key, we immediately jump to the "jumpdone" step. The problem is the jumpdone step expects the ExprState resnull and resvalue fields to be set (as they would be if we didn't abort hashing early due to the NULL), but when we aborted early, the ExprState fields never got set. This would result in inserting records into the hash table that would never match to any join partner, which is a waste of CPU and memory. Here we fix this by having EEOP_HASHDATUM_FIRST_STRICT and EEOP_HASHDATUM_NEXT32_STRICT populate the ExprState resnull and resvalue fields directly when the value to hash is NULL. Although Hash Agg and Hashed Subplans do use hashing from ExprStates, those were unaffected by this bug, as neither of those uses the STRICT op steps. Thanks to Tomas Vondra for finding the offending commit. Reported-by: Dan Stefura Author: David Rowley Discussion: https://postgr.es/m/YQBPR0101MB89738FB972FBD02A3640C6D3D6C92@YQBPR0101MB8973.CANPRD01.PROD.OUTLOOK.COM Backpatch-through: 18 --- src/backend/executor/execExprInterp.c | 8 ++++---- src/backend/jit/llvm/llvmjit_expr.c | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index f6093f36afa..80ac19baa25 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -1841,8 +1841,8 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) * ignoring NULL input values. We've nothing more to do after * finding a NULL. */ - *op->resnull = true; - *op->resvalue = (Datum) 0; + state->resnull = true; + state->resvalue = (Datum) 0; EEO_JUMP(op->d.hashdatum.jumpdone); } @@ -1889,8 +1889,8 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) * ignoring NULL input values. We've nothing more to do after * finding a NULL. */ - *op->resnull = true; - *op->resvalue = (Datum) 0; + state->resnull = true; + state->resvalue = (Datum) 0; EEO_JUMP(op->d.hashdatum.jumpdone); } else diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 2326223996e..e807e60e311 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -2111,11 +2111,12 @@ llvm_compile_expr(ExprState *state) LLVMPositionBuilderAtEnd(b, b_ifnullblock); /* - * In strict node, NULL inputs result in NULL. Save - * the NULL result and goto jumpdone. + * In strict mode, NULL inputs result in NULL. Save + * the NULL to the ExprState's resnull/resvalue fields + * directly, then goto jumpdone. */ - LLVMBuildStore(b, l_sbool_const(1), v_resnullp); - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_sbool_const(1), v_tmpisnullp); + LLVMBuildStore(b, l_sizet_const(0), v_tmpvaluep); LLVMBuildBr(b, opblocks[op->d.hashdatum.jumpdone]); } else From 4cc49cb70396e5a0941eabbe9b23f9c4a3b85c3a Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Fri, 31 Jul 2026 10:34:40 -0500 Subject: [PATCH 177/250] Fix autovacuum's database sorting. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When db_comparator() was updated to use pg_cmp_s32(), the arguments were listed in the wrong order. This caused autovacuum to sort the databases by their scores in ascending order instead of descending order. To fix, swap the arguments to pg_cmp_s32(). Oversight in commit 3b42bdb471. Reported-by: Хамидуллин Рустам Author: Хамидуллин Рустам Discussion: https://postgr.es/m/5c5a7984-b149-b505-7ad9-2a7766c65b55%40postgrespro.ru Backpatch-through: 17 --- src/backend/postmaster/autovacuum.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 66620fb4755..de43a87f74d 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -1071,8 +1071,8 @@ rebuild_database_list(Oid newdb) static int db_comparator(const void *a, const void *b) { - return pg_cmp_s32(((const avl_dbase *) a)->adl_score, - ((const avl_dbase *) b)->adl_score); + return pg_cmp_s32(((const avl_dbase *) b)->adl_score, + ((const avl_dbase *) a)->adl_score); } /* From 18f0de6b885a017dfba620e0b9a5f42a1bc16d25 Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Fri, 31 Jul 2026 11:57:07 -0400 Subject: [PATCH 178/250] Prevent walsummarizer from getting stuck at a timeline switch. As previously coded, walsummarizer only wants to read WAL from a file where the TimeLineID in the filename exactly matches the TimeLineID being summarized. But in some cases, when a timeline switch occurs, the WAL file from the old timeline is not archived, because it's never completely filled, so the only way to obtain the contents of that last partial segment is to read from the first segment on the new timeline. Teach WAL summarizer to do that, and add a test case to make sure that it works. Reported-by: Nick Ivanov Reviewed-by: Andrey Borodin Tested-by: Amit Kapila Reviewed-by: Srinath Reddy Sadipiralla Reviewed-by: Zhijie Hou Reviewed-by: Thom Brown Discussion: http://postgr.es/m/CA+Tgmobr27GpKDZx3_ezW2+C5_g18i+jSK3sGF_cR-_ESv5N5A@mail.gmail.com Backpatch-through: 17 --- src/backend/postmaster/walsummarizer.c | 173 ++++++++++++++++++++-- src/bin/pg_walsummary/meson.build | 1 + src/bin/pg_walsummary/t/003_tli_switch.pl | 149 +++++++++++++++++++ 3 files changed, 309 insertions(+), 14 deletions(-) create mode 100644 src/bin/pg_walsummary/t/003_tli_switch.pl diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index eec6ca08d5c..96bef8778ce 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -104,6 +104,8 @@ typedef struct bool historic; XLogRecPtr read_upto; bool end_of_wal; + int num_descendant_tlis; + TimeLineID *descendant_tlis; } SummarizerReadLocalXLogPrivate; /* Pointer to shared memory state. */ @@ -147,10 +149,14 @@ int wal_summary_keep_time = 10 * HOURS_PER_DAY * MINS_PER_HOUR; static void WalSummarizerShutdown(int code, Datum arg); static XLogRecPtr GetLatestLSN(TimeLineID *tli); +static XLogRecPtr WalSummarizerSwitchPoint(TimeLineID current_tli, List *tles, + int *num_descendant_tlis, + TimeLineID **descendant_tlis); static void ProcessWalSummarizerInterrupts(void); static XLogRecPtr SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, XLogRecPtr switch_lsn, - XLogRecPtr maximum_lsn); + XLogRecPtr maximum_lsn, + int num_descendant_tlis, TimeLineID *descendant_tlis); static void SummarizeDbaseRecord(XLogReaderState *xlogreader, BlockRefTable *brtab); static void SummarizeSmgrRecord(XLogReaderState *xlogreader, @@ -159,6 +165,9 @@ static void SummarizeXactRecord(XLogReaderState *xlogreader, BlockRefTable *brtab); static bool SummarizeXlogRecord(XLogReaderState *xlogreader, bool *new_fast_forward); +static void summarizer_wal_segment_open(XLogReaderState *state, + XLogSegNo nextSegNo, + TimeLineID *tli_p); static int summarizer_read_local_xlog_page(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, @@ -222,16 +231,19 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) * true if 'current_lsn' is known to be the start of a WAL record or WAL * segment, and false if it might be in the middle of a record someplace. * - * 'switch_lsn' and 'switch_tli', if set, are the LSN at which we need to - * switch to a new timeline and the timeline to which we need to switch. - * If not set, we either haven't figured out the answers yet or we're - * already on the latest timeline. + * 'switch_lsn', is the LSN at which we need to switch to a new timeline. + * If not set, we either haven't figured out the answer yet or we're + * already on the latest timeline. 'descendant_tlis' stores an array of + * future timeline IDs to which we know we'll need to switch, and + * 'num_descendant_tlis' is the length of that array. The first element of + * the array is the first timeline to which we will need to switch. */ XLogRecPtr current_lsn; TimeLineID current_tli; bool exact; XLogRecPtr switch_lsn = InvalidXLogRecPtr; - TimeLineID switch_tli = 0; + int num_descendant_tlis = 0; + TimeLineID *descendant_tlis = NULL; Assert(startup_data_len == 0); @@ -384,11 +396,33 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) if (current_tli != latest_tli && XLogRecPtrIsInvalid(switch_lsn)) { List *tles = readTimeLineHistory(latest_tli); + int new_num_descendant_tlis; + TimeLineID *new_descendant_tlis; - switch_lsn = tliSwitchPoint(current_tli, tles, &switch_tli); + /* + * Make sure that the array of descendant TLIs get stored into + * TopMemoryContext. + */ + MemoryContextSwitchTo(TopMemoryContext); + switch_lsn = WalSummarizerSwitchPoint(current_tli, tles, + &new_num_descendant_tlis, + &new_descendant_tlis); + MemoryContextSwitchTo(context); + + /* + * Free any old array of descendant TLIs and install the new + * values. + */ + if (descendant_tlis != NULL) + pfree(descendant_tlis); + num_descendant_tlis = new_num_descendant_tlis; + descendant_tlis = new_descendant_tlis; + + /* Debug message. */ ereport(DEBUG1, errmsg_internal("switch point from TLI %u to TLI %u is at %X/%X", - current_tli, switch_tli, LSN_FORMAT_ARGS(switch_lsn))); + current_tli, descendant_tlis[0], + LSN_FORMAT_ARGS(switch_lsn))); } /* @@ -399,12 +433,15 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) if (!XLogRecPtrIsInvalid(switch_lsn) && current_lsn >= switch_lsn) { /* Restart summarization from switch point. */ - current_tli = switch_tli; + Assert(num_descendant_tlis > 0); + current_tli = descendant_tlis[0]; current_lsn = switch_lsn; - /* Next timeline and switch point, if any, not yet known. */ + /* Switch point, if any, and future TLIs, not yet known. */ switch_lsn = InvalidXLogRecPtr; - switch_tli = 0; + num_descendant_tlis = 0; + pfree(descendant_tlis); + descendant_tlis = NULL; /* Update (really, rewind, if needed) state in shared memory. */ LWLockAcquire(WALSummarizerLock, LW_EXCLUSIVE); @@ -421,7 +458,8 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) maximum_lsn = XLogRecPtrIsValid(switch_lsn) ? switch_lsn : latest_lsn; end_of_summary_lsn = SummarizeWAL(current_tli, current_lsn, exact, - switch_lsn, maximum_lsn); + switch_lsn, maximum_lsn, + num_descendant_tlis, descendant_tlis); Assert(!XLogRecPtrIsInvalid(end_of_summary_lsn)); Assert(end_of_summary_lsn >= current_lsn); @@ -857,6 +895,62 @@ GetLatestLSN(TimeLineID *tli) } } +/* + * Compute the LSN at which we switched from current_tli to some later timeline. + * 'tles' must be the timeline history of the latest timeline. + * + * As a side effect, we set *num_descendant_tlis to the number of later TLIs that + * appear in the timeline history, and *descendant_tlis to an array of those TLIs, + * starting with immediate successor of current_tli. + */ +static XLogRecPtr +WalSummarizerSwitchPoint(TimeLineID current_tli, List *tles, + int *num_descendant_tlis, TimeLineID **descendant_tlis) +{ + XLogRecPtr switch_lsn = InvalidXLogRecPtr; + int count = 0; + + /* + * Find the switch point and, at the same time, count the number of TLIs + * in this history that are descendants of that TLI. + */ + foreach_ptr(TimeLineHistoryEntry, tle, tles) + { + if (tle->tli == current_tli) + { + switch_lsn = tle->end; + break; + } + ++count; + } + + /* Sanity checks. */ + if (!XLogRecPtrIsValid(switch_lsn)) + ereport(ERROR, + (errmsg("requested timeline %u is not in this server's history", + current_tli))); + if (count == 0) + elog(ERROR, "cannot compute switch point for current TLI %u", current_tli); + + /* + * Generate an array of TLIs that are part of this history and descendants + * of current_tli. The TLE list starts with the newest timeline and works + * backward toward older timelines; we want the opposite ordering. + */ + *num_descendant_tlis = count; + *descendant_tlis = palloc_array(TimeLineID, count); + for (int i = 0; i < count; ++i) + { + TimeLineHistoryEntry *tle; + + tle = (TimeLineHistoryEntry *) list_nth(tles, count - i - 1); + (*descendant_tlis)[i] = tle->tli; + } + + /* Return value is the switchpoint. */ + return switch_lsn; +} + /* * Interrupt handler for main loop of WAL summarizer process. */ @@ -910,7 +1004,8 @@ ProcessWalSummarizerInterrupts(void) */ static XLogRecPtr SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, - XLogRecPtr switch_lsn, XLogRecPtr maximum_lsn) + XLogRecPtr switch_lsn, XLogRecPtr maximum_lsn, + int num_descendant_tlis, TimeLineID *descendant_tlis) { SummarizerReadLocalXLogPrivate *private_data; XLogReaderState *xlogreader; @@ -928,11 +1023,13 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, private_data->tli = tli; private_data->historic = !XLogRecPtrIsInvalid(switch_lsn); private_data->read_upto = maximum_lsn; + private_data->num_descendant_tlis = num_descendant_tlis; + private_data->descendant_tlis = descendant_tlis; /* Create xlogreader. */ xlogreader = XLogReaderAllocate(wal_segment_size, NULL, XL_ROUTINE(.page_read = &summarizer_read_local_xlog_page, - .segment_open = &wal_segment_open, + .segment_open = &summarizer_wal_segment_open, .segment_close = &wal_segment_close), private_data); if (xlogreader == NULL) @@ -1487,6 +1584,54 @@ SummarizeXlogRecord(XLogReaderState *xlogreader, bool *new_fast_forward) return true; } +/* + * Similar to wal_segment_open, but checks for a file on any descendant timelines + * known to us if no file is found on the requested timeline. + */ +static void +summarizer_wal_segment_open(XLogReaderState *state, XLogSegNo nextSegNo, + TimeLineID *tli_p) +{ + SummarizerReadLocalXLogPrivate *private_data = state->private_data; + int count = 0; + TimeLineID tli = *tli_p; + char path[MAXPGPATH]; + + for (;;) + { + XLogFilePath(path, tli, nextSegNo, state->segcxt.ws_segsize); + state->seg.ws_file = BasicOpenFile(path, O_RDONLY | PG_BINARY); + if (state->seg.ws_file >= 0) + { + *tli_p = tli; + return; + } + + /* + * If the error is anything other than file-not-found, complain at + * once. + */ + if (errno != ENOENT) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", + path))); + + /* Try other timelines, if any remain. */ + if (count >= private_data->num_descendant_tlis) + break; + tli = private_data->descendant_tlis[count]; + ++count; + } + + /* Complain about the originally requested filename. */ + XLogFilePath(path, *tli_p, nextSegNo, state->segcxt.ws_segsize); + ereport(ERROR, + (errcode_for_file_access(), + errmsg("requested WAL segment %s has already been removed", + path))); +} + /* * Similar to read_local_xlog_page, but limited to read from one particular * timeline. If the end of WAL is reached, it will wait for more if reading diff --git a/src/bin/pg_walsummary/meson.build b/src/bin/pg_walsummary/meson.build index fe9af967b12..7e8e7b4216e 100644 --- a/src/bin/pg_walsummary/meson.build +++ b/src/bin/pg_walsummary/meson.build @@ -25,6 +25,7 @@ tests += { 'tests': [ 't/001_basic.pl', 't/002_blocks.pl', + 't/003_tli_switch.pl', ], } } diff --git a/src/bin/pg_walsummary/t/003_tli_switch.pl b/src/bin/pg_walsummary/t/003_tli_switch.pl new file mode 100644 index 00000000000..28e09f3260a --- /dev/null +++ b/src/bin/pg_walsummary/t/003_tli_switch.pl @@ -0,0 +1,149 @@ +# Copyright (c) 2021-2026, PostgreSQL Global Development Group +# +# In the original version of the WAL summarizer code, we were only willing +# to read WAL for a given TLI from a file with that exact TLI encoded into +# the filename. This could result in WAL summarization running on an archiving +# standby getting stuck. +# +# The reason for the problem is that when a new primary is promoted, the +# partial file that ends the old timeline is renamed, giving it a ".partial" +# suffix, meaning that it will be ignored by both recovery and by the WAL +# summarizer. The bytes that appear at the start of that segment will be copied +# into the first segment on the new timeline, and recovery was able to read +# them from there and work as expected. However, the WAL summarizer was +# unwilling to do the same thing, so it got stuck. This test aims to validate +# that this bug has been fixed. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Set up node1 as primary. +my $node1 = PostgreSQL::Test::Cluster->new('node1'); +$node1->init(allows_streaming => 1); +$node1->append_conf('postgresql.conf', <start; + +# Set up node2 as a standby for node1. Use archive_mode=always, to make sure it +# archives both before and after promotion. +$node1->backup('backup1'); +my $node2 = PostgreSQL::Test::Cluster->new('node2'); +$node2->init_from_backup($node1, 'backup1', has_streaming => 1); +$node2->enable_archiving(); +$node2->append_conf('postgresql.conf', <start; + +# Wait for node2 to catch up. +$node1->wait_for_replay_catchup($node2); + +# Set up node3 as a standby for node2. We want it to fetch WAL only from the +# archive, so we clear primary_conninfo. We don't want long delays during the +# test, so we reduce wal_retrieve_retry_interval. We also don't want it to try +# to archive anything to node2's archive, but at the same time, we don't want +# it to remove WAL before we enable WAL summarization. To accomplish that, we +# set archive_command to the empty string. +$node2->backup('backup2'); +my $node3 = PostgreSQL::Test::Cluster->new('node3'); +$node3->init_from_backup($node2, 'backup2', has_restoring => 1); +$node3->append_conf('postgresql.conf', <start; + +# Create a new, partially-filled WAL segment on node1. +$node1->safe_psql('postgres', <wait_for_replay_catchup($node2); + +# Record the WAL insert LSN on node1, so we can later verify that summarization +# on node3 advances past this point. +my $node1_final_lsn = $node1->safe_psql('postgres', + 'SELECT pg_current_wal_insert_lsn()'); + +# Promote node2. This creates a timeline switch that node3 must follow. +$node2->promote; +$node2->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';"); + +# Cause the partial segment to get archived on the *new* timeline. +# +# In more detail: the WAL segment that contains the current insert LSN exists +# on timeline 1, but since all we did is CREATE TABLE dummy (), it wasn't full. +# We're now running on timeline 2, and pg_switch_wal() fills up the rest of the +# segment. So the full segment should get archived on timeline 2, but not on +# timeline 1. We do a CHECKPOINT here to make sure that the summarizer tries +# to progress. +my $node2_switch_lsn = + $node2->safe_psql('postgres', 'SELECT pg_switch_wal()'); +$node2->safe_psql('postgres', 'CHECKPOINT'); + +# Wait until replay has reached TLI 2 on node3, and then start the WAL +# summarizer. If node3 is started with the summarizer already enabled, then +# it may try to fetch the partial segment from timeline 1 before it learns +# about timeline 2. If that happens, it will error out, wait 10 seconds, and +# retry, slowing down the test. This avoids that. +# +# Since the pg_switch_wal() above was executed after promotion, its return +# value is past the timeline switch point, so once replay reaches it, node3 +# must be replaying from TLI 2. +# +# We set log_min_messages=debug1 at the same time we enable WAL summarization +# so that we get useful debug messages if there's any problem. +$node3->poll_query_until('postgres', + "SELECT pg_last_wal_replay_lsn() >= '$node2_switch_lsn'::pg_lsn") + or die "TLI 2 not reached on node3"; +$node3->append_conf('postgresql.conf', <reload; + +# Wait for WAL summarization on node3 to advance past the pre-promotion LSN. +# If the bug is present, the summarizer gets stuck trying to open the old +# timeline's segment file. +my $result = $node3->poll_query_until('postgres', <safe_psql('postgres', <safe_psql('postgres', <= '$node1_final_lsn' ORDER BY start_lsn +EOM +my @summary_lines = split(/\n/, $summaries); +ok(@summary_lines > 0, "at least one summary from LSN $node1_final_lsn or later"); + +# We expect the new summaries to be empty, because we have not actually touched +# any block data (and we disabled autovacuum from the start). +for my $line (@summary_lines) +{ + my ($tli, $start_lsn, $end_lsn) = split(/\|/, $line); + my $filename = sprintf "%s/pg_wal/summaries/%08s%08s%08s%08s%08s.summary", + $node3->data_dir, $tli, + split(m@/@, $start_lsn), + split(m@/@, $end_lsn); + my ($stdout, $stderr) = run_command([ 'pg_walsummary', $filename ]); + is($stdout, '', "pg_walsummary TLI $tli $start_lsn-$end_lsn: no blocks"); + is($stderr, '', "pg_walsummary TLI $tli $start_lsn-$end_lsn: no error"); +} + +done_testing(); From fff8c86759659a070e014ffd7295e93a11a1479c Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Fri, 31 Jul 2026 11:19:37 -0700 Subject: [PATCH 179/250] oauth: Add unit tests for multiplexer handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (This is a late cherry-pick of 4e1e41733, now that the new suite has proven its stability, to ensure coverage for this code in PG18 as the later branches diverge.) To better record the internal behaviors of oauth-curl.c, add a unit test suite for the socket and timer handling code. This is all based on TAP and driven by our existing Test::More infrastructure. This commit is a replay of 1443b6c0e, which was reverted due to buildfarm failures. Compared with that, this version protects the build targets in the Makefile with a with_libcurl conditional, and it tweaks the code style in 001_oauth.pl. Reviewed-by: Dagfinn Ilmari Mannsåker Reviewed-by: Andrew Dunstan Discussion: https://postgr.es/m/CAOYmi+nDZxJHaWj9_jRSyf8uMToCADAmOfJEggsKW-kY7aUwHA@mail.gmail.com Discussion: https://postgr.es/m/CAOYmi+m=xY0P_uAzAP_884uF-GhQ3wrineGwc9AEnb6fYxVqVQ@mail.gmail.com --- src/interfaces/libpq-oauth/Makefile | 36 +- src/interfaces/libpq-oauth/meson.build | 35 ++ src/interfaces/libpq-oauth/t/001_oauth.pl | 24 + src/interfaces/libpq-oauth/test-oauth-curl.c | 527 +++++++++++++++++++ 4 files changed, 618 insertions(+), 4 deletions(-) create mode 100644 src/interfaces/libpq-oauth/t/001_oauth.pl create mode 100644 src/interfaces/libpq-oauth/test-oauth-curl.c diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile index 71cd1829720..51145f085a8 100644 --- a/src/interfaces/libpq-oauth/Makefile +++ b/src/interfaces/libpq-oauth/Makefile @@ -55,10 +55,6 @@ SHLIB_EXPORTS = exports.txt # Disable -bundle_loader on macOS. BE_DLLLIBS = -# By default, a library without an SONAME doesn't get a static library, so we -# add it to the build explicitly. -all: all-lib all-static-lib - # Shared library stuff include $(top_srcdir)/src/Makefile.shlib @@ -67,6 +63,28 @@ include $(top_srcdir)/src/Makefile.shlib %_shlib.o: %.c %.o $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ +.PHONY: all-tests +all-tests: oauth_tests$(X) + +oauth_tests$(X): test-oauth-curl.o oauth-utils.o $(WIN32RES) | submake-libpgport submake-libpq + $(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(SHLIB_LINK) -o $@ + +# +# Top-Level Targets +# +# The existence of a t/ folder induces the buildfarm to run Make directly on +# this subdirectory, bypassing the recursion skip in src/interfaces/Makefile. +# Wrap the standard build targets in a with_libcurl conditional to avoid +# building OAuth code on platforms that haven't requested it. (The "clean"-style +# targets remain available.) +# + +ifeq ($(with_libcurl), yes) + +# By default, a library without an SONAME doesn't get a static library, so we +# add it to the build explicitly. +all: all-lib all-static-lib + # Ignore the standard rules for SONAME-less installation; we want both the # static and shared libraries to go into libdir. install: all installdirs $(stlib) $(shlib) @@ -76,9 +94,19 @@ install: all installdirs $(stlib) $(shlib) installdirs: $(MKDIR_P) '$(DESTDIR)$(libdir)' +check: all-tests + $(prove_check) + +installcheck: all-tests + $(prove_installcheck) + +endif # with_libcurl + uninstall: rm -f '$(DESTDIR)$(libdir)/$(stlib)' rm -f '$(DESTDIR)$(libdir)/$(shlib)' clean distclean: clean-lib rm -f $(OBJS) $(OBJS_STATIC) $(OBJS_SHLIB) + rm -f test-oauth-curl.o oauth_tests$(X) + rm -rf tmp_check diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build index 4f006e2c381..881e3f24f10 100644 --- a/src/interfaces/libpq-oauth/meson.build +++ b/src/interfaces/libpq-oauth/meson.build @@ -47,3 +47,38 @@ libpq_oauth_so = shared_module(libpq_oauth_name, link_args: export_fmt.format(export_file.full_path()), kwargs: default_lib_args, ) + +libpq_oauth_test_deps = [] + +oauth_test_sources = files('test-oauth-curl.c') + libpq_oauth_so_sources + +if host_system == 'windows' + oauth_test_sources += rc_bin_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'oauth_tests', + '--FILEDESC', 'OAuth unit test program',]) +endif + +libpq_oauth_test_deps += executable('oauth_tests', + oauth_test_sources, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + kwargs: default_bin_args + { + 'c_args': default_bin_args.get('c_args', []) + libpq_oauth_so_c_args, + 'c_pch': pch_postgres_fe_h, + 'include_directories': [libpq_inc, postgres_inc], + 'install': false, + } +) + +testprep_targets += libpq_oauth_test_deps + +tests += { + 'name': 'libpq-oauth', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_oauth.pl', + ], + 'deps': libpq_oauth_test_deps, + }, +} diff --git a/src/interfaces/libpq-oauth/t/001_oauth.pl b/src/interfaces/libpq-oauth/t/001_oauth.pl new file mode 100644 index 00000000000..6c972056bbd --- /dev/null +++ b/src/interfaces/libpq-oauth/t/001_oauth.pl @@ -0,0 +1,24 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Utils; +use Test::More; + +# Defer entirely to the oauth_tests executable. stdout/err is routed through +# Test::More so that our logging infrastructure can handle it correctly. Using +# IPC::Run::new_chunker seems to help interleave the two streams a little better +# than without. +# +# TODO: prove can also deal with native executables itself, which we could +# probably make use of via PROVE_TESTS on the Makefile side. But the Meson setup +# calls Perl directly, which would require more code to work around... and +# there's still the matter of logging. +my $builder = Test::More->builder; +my $out = $builder->output; +my $err = $builder->failure_output; + +IPC::Run::run ['oauth_tests'], + '>' => (IPC::Run::new_chunker, sub { $out->print($_[0]) }), + '2>' => (IPC::Run::new_chunker, sub { $err->print($_[0]) }) + or die "oauth_tests returned $?"; diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c new file mode 100644 index 00000000000..8263aff2f4a --- /dev/null +++ b/src/interfaces/libpq-oauth/test-oauth-curl.c @@ -0,0 +1,527 @@ +/* + * test-oauth-curl.c + * + * A unit test driver for libpq-oauth. This #includes oauth-curl.c, which lets + * the tests reference static functions and other internals. + * + * USE_ASSERT_CHECKING is required, to make it easy for tests to wrap + * must-succeed code as part of test setup. + * + * Copyright (c) 2025, PostgreSQL Global Development Group + */ + +#include "oauth-curl.c" + +#include + +#ifdef USE_ASSERT_CHECKING + +/* + * TAP Helpers + */ + +static int num_tests = 0; + +/* + * Reports ok/not ok to the TAP stream on stdout. + */ +#define ok(OK, TEST) \ + ok_impl(OK, TEST, #OK, __FILE__, __LINE__) + +static bool +ok_impl(bool ok, const char *test, const char *teststr, const char *file, int line) +{ + printf("%sok %d - %s\n", ok ? "" : "not ", ++num_tests, test); + + if (!ok) + { + printf("# at %s:%d:\n", file, line); + printf("# expression is false: %s\n", teststr); + } + + return ok; +} + +/* + * Like ok(this == that), but with more diagnostics on failure. + * + * Only works on ints, but luckily that's all we need here. Note that the much + * simpler-looking macro implementation + * + * is_diag(ok(THIS == THAT, TEST), THIS, #THIS, THAT, #THAT) + * + * suffers from multiple evaluation of the macro arguments... + */ +#define is(THIS, THAT, TEST) \ + do { \ + int this_ = (THIS), \ + that_ = (THAT); \ + is_diag( \ + ok_impl(this_ == that_, TEST, #THIS " == " #THAT, __FILE__, __LINE__), \ + this_, #THIS, that_, #THAT \ + ); \ + } while (0) + +static bool +is_diag(bool ok, int this, const char *thisstr, int that, const char *thatstr) +{ + if (!ok) + printf("# %s = %d; %s = %d\n", thisstr, this, thatstr, that); + + return ok; +} + +/* + * Utilities + */ + +/* + * Creates a partially-initialized async_ctx for the purposes of testing. Free + * with free_test_actx(). + */ +static struct async_ctx * +init_test_actx(void) +{ + struct async_ctx *actx; + + actx = calloc(1, sizeof(*actx)); + Assert(actx); + + actx->mux = PGINVALID_SOCKET; + actx->timerfd = -1; + actx->debugging = true; + + initPQExpBuffer(&actx->errbuf); + + Assert(setup_multiplexer(actx)); + + return actx; +} + +static void +free_test_actx(struct async_ctx *actx) +{ + termPQExpBuffer(&actx->errbuf); + + if (actx->mux != PGINVALID_SOCKET) + close(actx->mux); + if (actx->timerfd >= 0) + close(actx->timerfd); + + free(actx); +} + +static char dummy_buf[4 * 1024]; /* for fill_pipe/drain_pipe */ + +/* + * Writes to the write side of a pipe until it won't take any more data. Returns + * the amount written. + */ +static ssize_t +fill_pipe(int fd) +{ + int mode; + ssize_t written = 0; + + /* Don't block. */ + Assert((mode = fcntl(fd, F_GETFL)) != -1); + Assert(fcntl(fd, F_SETFL, mode | O_NONBLOCK) == 0); + + while (true) + { + ssize_t w; + + w = write(fd, dummy_buf, sizeof(dummy_buf)); + if (w < 0) + { + if (errno != EAGAIN && errno != EWOULDBLOCK) + { + perror("write to pipe"); + written = -1; + } + break; + } + + written += w; + } + + /* Reset the descriptor flags. */ + Assert(fcntl(fd, F_SETFD, mode) == 0); + + return written; +} + +/* + * Drains the requested amount of data from the read side of a pipe. + */ +static bool +drain_pipe(int fd, ssize_t n) +{ + Assert(n > 0); + + while (n) + { + size_t to_read = (n <= sizeof(dummy_buf)) ? n : sizeof(dummy_buf); + ssize_t drained; + + drained = read(fd, dummy_buf, to_read); + if (drained < 0) + { + perror("read from pipe"); + return false; + } + + n -= drained; + } + + return true; +} + +/* + * Tests whether the multiplexer is marked ready by the deadline. This is a + * macro so that file/line information makes sense during failures. + * + * NB: our current multiplexer implementations (epoll/kqueue) are *readable* + * when the underlying libcurl sockets are *writable*. This behavior is pinned + * here to record that expectation; PGRES_POLLING_READING is hardcoded + * throughout the flow and would need to be changed if a new multiplexer does + * something different. + */ +#define mux_is_ready(MUX, DEADLINE, TEST) \ + do { \ + int res_ = PQsocketPoll(MUX, 1, 0, DEADLINE); \ + Assert(res_ != -1); \ + ok(res_ > 0, "multiplexer is ready " TEST); \ + } while (0) + +/* + * The opposite of mux_is_ready(). + */ +#define mux_is_not_ready(MUX, TEST) \ + do { \ + int res_ = PQsocketPoll(MUX, 1, 0, 0); \ + Assert(res_ != -1); \ + is(res_, 0, "multiplexer is not ready " TEST); \ + } while (0) + +/* + * Test Suites + */ + +/* Per-suite timeout. Set via the PG_TEST_TIMEOUT_DEFAULT envvar. */ +static pg_usec_time_t timeout_us = 180 * 1000 * 1000; + +static void +test_set_timer(void) +{ + struct async_ctx *actx = init_test_actx(); + const pg_usec_time_t deadline = PQgetCurrentTimeUSec() + timeout_us; + + printf("# test_set_timer\n"); + + /* A zero-duration timer should result in a near-immediate ready signal. */ + Assert(set_timer(actx, 0)); + mux_is_ready(actx->mux, deadline, "when timer expires"); + is(timer_expired(actx), 1, "timer_expired() returns 1 when timer expires"); + + /* Resetting the timer far in the future should unset the ready signal. */ + Assert(set_timer(actx, INT_MAX)); + mux_is_not_ready(actx->mux, "when timer is reset to the future"); + is(timer_expired(actx), 0, "timer_expired() returns 0 with unexpired timer"); + + /* Setting another zero-duration timer should override the previous one. */ + Assert(set_timer(actx, 0)); + mux_is_ready(actx->mux, deadline, "when timer is re-expired"); + is(timer_expired(actx), 1, "timer_expired() returns 1 when timer is re-expired"); + + /* And disabling that timer should once again unset the ready signal. */ + Assert(set_timer(actx, -1)); + mux_is_not_ready(actx->mux, "when timer is unset"); + is(timer_expired(actx), 0, "timer_expired() returns 0 when timer is unset"); + + { + bool expired; + + /* Make sure drain_timer_events() functions correctly as well. */ + Assert(set_timer(actx, 0)); + mux_is_ready(actx->mux, deadline, "when timer is re-expired (drain_timer_events)"); + + Assert(drain_timer_events(actx, &expired)); + mux_is_not_ready(actx->mux, "when timer is drained after expiring"); + is(expired, 1, "drain_timer_events() reports expiration"); + is(timer_expired(actx), 0, "timer_expired() returns 0 after timer is drained"); + + /* A second drain should do nothing. */ + Assert(drain_timer_events(actx, &expired)); + mux_is_not_ready(actx->mux, "when timer is drained a second time"); + is(expired, 0, "drain_timer_events() reports no expiration"); + is(timer_expired(actx), 0, "timer_expired() still returns 0"); + } + + free_test_actx(actx); +} + +static void +test_register_socket(void) +{ + struct async_ctx *actx = init_test_actx(); + int pipefd[2]; + int rfd, + wfd; + bool bidirectional; + + /* Create a local pipe for communication. */ + Assert(pipe(pipefd) == 0); + rfd = pipefd[0]; + wfd = pipefd[1]; + + /* + * Some platforms (FreeBSD) implement bidirectional pipes, affecting the + * behavior of some of these tests. Store that knowledge for later. + */ + bidirectional = PQsocketPoll(rfd /* read */ , 0, 1 /* write */ , 0) > 0; + + /* + * This suite runs twice -- once using CURL_POLL_IN/CURL_POLL_OUT for + * read/write operations, respectively, and once using CURL_POLL_INOUT for + * both sides. + */ + for (int inout = 0; inout < 2; inout++) + { + const int in_event = inout ? CURL_POLL_INOUT : CURL_POLL_IN; + const int out_event = inout ? CURL_POLL_INOUT : CURL_POLL_OUT; + const pg_usec_time_t deadline = PQgetCurrentTimeUSec() + timeout_us; + size_t bidi_pipe_size = 0; /* silence compiler warnings */ + + printf("# test_register_socket %s\n", inout ? "(INOUT)" : ""); + + /* + * At the start of the test, the read side should be blocked and the + * write side should be open. (There's a mistake at the end of this + * loop otherwise.) + */ + Assert(PQsocketPoll(rfd, 1, 0, 0) == 0); + Assert(PQsocketPoll(wfd, 0, 1, 0) > 0); + + /* + * For bidirectional systems, emulate unidirectional behavior here by + * filling up the "read side" of the pipe. + */ + if (bidirectional) + Assert((bidi_pipe_size = fill_pipe(rfd)) > 0); + + /* Listen on the read side. The multiplexer shouldn't be ready yet. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when fd is not readable"); + + /* Writing to the pipe should result in a read-ready multiplexer. */ + Assert(write(wfd, "x", 1) == 1); + mux_is_ready(actx->mux, deadline, "when fd is readable"); + + /* + * Update the registration to wait on write events instead. The + * multiplexer should be unset. + */ + Assert(register_socket(NULL, rfd, CURL_POLL_OUT, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when waiting for writes on readable fd"); + + /* Re-register for read events. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + mux_is_ready(actx->mux, deadline, "when waiting for reads again"); + + /* Stop listening. The multiplexer should be unset. */ + Assert(register_socket(NULL, rfd, CURL_POLL_REMOVE, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when readable fd is removed"); + + /* Listen again. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + mux_is_ready(actx->mux, deadline, "when readable fd is re-added"); + + /* + * Draining the pipe should unset the multiplexer again, once the old + * event is cleared. + */ + Assert(drain_pipe(rfd, 1)); + Assert(comb_multiplexer(actx)); + mux_is_not_ready(actx->mux, "when fd is drained"); + + /* Undo any unidirectional emulation. */ + if (bidirectional) + Assert(drain_pipe(wfd, bidi_pipe_size)); + + /* Listen on the write side. An empty buffer should be writable. */ + Assert(register_socket(NULL, rfd, CURL_POLL_REMOVE, actx, NULL) == 0); + Assert(register_socket(NULL, wfd, out_event, actx, NULL) == 0); + mux_is_ready(actx->mux, deadline, "when fd is writable"); + + /* As above, wait on read events instead. */ + Assert(register_socket(NULL, wfd, CURL_POLL_IN, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when waiting for reads on writable fd"); + + /* Re-register for write events. */ + Assert(register_socket(NULL, wfd, out_event, actx, NULL) == 0); + mux_is_ready(actx->mux, deadline, "when waiting for writes again"); + + { + ssize_t written; + + /* + * Fill the pipe. Once the old writable event is cleared, the mux + * should not be ready. + */ + Assert((written = fill_pipe(wfd)) > 0); + printf("# pipe buffer is full at %zd bytes\n", written); + + Assert(comb_multiplexer(actx)); + mux_is_not_ready(actx->mux, "when fd buffer is full"); + + /* Drain the pipe again. */ + Assert(drain_pipe(rfd, written)); + mux_is_ready(actx->mux, deadline, "when fd buffer is drained"); + } + + /* Stop listening. */ + Assert(register_socket(NULL, wfd, CURL_POLL_REMOVE, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when fd is removed"); + + /* Make sure an expired timer doesn't interfere with event draining. */ + { + bool expired; + + /* Make the rfd appear unidirectional if necessary. */ + if (bidirectional) + Assert((bidi_pipe_size = fill_pipe(rfd)) > 0); + + /* Set the timer and wait for it to expire. */ + Assert(set_timer(actx, 0)); + Assert(PQsocketPoll(actx->timerfd, 1, 0, deadline) > 0); + is(timer_expired(actx), 1, "timer is expired"); + + /* Register for read events and make the fd readable. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + Assert(write(wfd, "x", 1) == 1); + mux_is_ready(actx->mux, deadline, "when fd is readable and timer expired"); + + /* + * Draining the pipe should unset the multiplexer again, once the + * old event is drained and the timer is reset. + * + * Order matters, since comb_multiplexer() doesn't have to remove + * stale events when active events exist. Follow the call sequence + * used in the code: drain the timer expiration, drain the pipe, + * then clear the stale events. + */ + Assert(drain_timer_events(actx, &expired)); + Assert(drain_pipe(rfd, 1)); + Assert(comb_multiplexer(actx)); + + is(expired, 1, "drain_timer_events() reports expiration"); + is(timer_expired(actx), 0, "timer is no longer expired"); + mux_is_not_ready(actx->mux, "when fd is drained and timer reset"); + + /* Stop listening. */ + Assert(register_socket(NULL, rfd, CURL_POLL_REMOVE, actx, NULL) == 0); + + /* Undo any unidirectional emulation. */ + if (bidirectional) + Assert(drain_pipe(wfd, bidi_pipe_size)); + } + + /* Ensure comb_multiplexer() can handle multiple stale events. */ + { + int rfd2, + wfd2; + + /* Create a second local pipe. */ + Assert(pipe(pipefd) == 0); + rfd2 = pipefd[0]; + wfd2 = pipefd[1]; + + /* Make both rfds appear unidirectional if necessary. */ + if (bidirectional) + { + Assert((bidi_pipe_size = fill_pipe(rfd)) > 0); + Assert(fill_pipe(rfd2) == bidi_pipe_size); + } + + /* Register for read events on both fds, and make them readable. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + Assert(register_socket(NULL, rfd2, in_event, actx, NULL) == 0); + + Assert(write(wfd, "x", 1) == 1); + Assert(write(wfd2, "x", 1) == 1); + + mux_is_ready(actx->mux, deadline, "when two fds are readable"); + + /* + * Drain both fds. comb_multiplexer() should then ensure that the + * mux is no longer readable. + */ + Assert(drain_pipe(rfd, 1)); + Assert(drain_pipe(rfd2, 1)); + Assert(comb_multiplexer(actx)); + mux_is_not_ready(actx->mux, "when two fds are drained"); + + /* Stop listening. */ + Assert(register_socket(NULL, rfd, CURL_POLL_REMOVE, actx, NULL) == 0); + Assert(register_socket(NULL, rfd2, CURL_POLL_REMOVE, actx, NULL) == 0); + + /* Undo any unidirectional emulation. */ + if (bidirectional) + { + Assert(drain_pipe(wfd, bidi_pipe_size)); + Assert(drain_pipe(wfd2, bidi_pipe_size)); + } + + close(rfd2); + close(wfd2); + } + } + + close(rfd); + close(wfd); + free_test_actx(actx); +} + +int +main(int argc, char *argv[]) +{ + const char *timeout; + + /* Grab the default timeout. */ + timeout = getenv("PG_TEST_TIMEOUT_DEFAULT"); + if (timeout) + { + int timeout_s = atoi(timeout); + + if (timeout_s > 0) + timeout_us = timeout_s * 1000 * 1000; + } + + /* + * Set up line buffering for our output, to let stderr interleave in the + * log files. + */ + setvbuf(stdout, NULL, PG_IOLBF, 0); + + test_set_timer(); + test_register_socket(); + + printf("1..%d\n", num_tests); + return 0; +} + +#else /* !USE_ASSERT_CHECKING */ + +/* + * Skip the test suite when we don't have assertions. + */ +int +main(int argc, char *argv[]) +{ + printf("1..0 # skip: cassert is not enabled\n"); + + return 0; +} + +#endif /* USE_ASSERT_CHECKING */ From 74169d3a1d695556ad81ef7a9c256daf0d554da1 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Fri, 31 Jul 2026 11:19:44 -0700 Subject: [PATCH 180/250] libpq-oauth: Avoid overflow for very large intervals The slow_down interval parsing code checks explicitly for overflow, but since it does that after the signed overflow has already occurred, we end up inviting undefined behavior from the compiler anyway. Use checked arithmetic instead. set_timer() takes a long int in order to interface nicely with libcurl, so use an int32 as the interval counter and clamp to LONG_MAX during conversion to milliseconds. Backpatch to 18, where libpq-oauth was introduced. Reported-by: Andres Freund Reviewed-by: Daniel Gustafsson Discussion: https://postgr.es/m/qtclihmrkq67ach3xjxyi4qcksstin5qxwsnkqefkmotxwh4g6%40ae2bj6jvcmry Backpatch-through: 18 --- src/interfaces/libpq-oauth/oauth-curl.c | 35 ++++++++++++++++++------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c index 70fac3ea243..4d7ae1ae790 100644 --- a/src/interfaces/libpq-oauth/oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -28,6 +28,7 @@ #error libpq-oauth is not supported on this platform #endif +#include "common/int.h" #include "common/jsonapi.h" #include "fe-auth-oauth.h" #include "mb/pg_wchar.h" @@ -144,7 +145,7 @@ struct device_authz /* Fields below are parsed from the corresponding string above. */ int expires_in; - int interval; + int32 interval; }; static void @@ -976,7 +977,7 @@ parse_json_number(const char *s) * expensive network polling loop.) Tests may remove the lower bound with * PGOAUTHDEBUG, for improved performance. */ -static int +static int32 parse_interval(struct async_ctx *actx, const char *interval_str) { double parsed; @@ -987,8 +988,8 @@ parse_interval(struct async_ctx *actx, const char *interval_str) if (parsed < 1) return actx->debugging ? 0 : 1; - else if (parsed >= INT_MAX) - return INT_MAX; + else if (parsed >= INT32_MAX) + return INT32_MAX; return parsed; } @@ -2590,10 +2591,7 @@ handle_token_response(struct async_ctx *actx, char **token) */ if (strcmp(err->error, "slow_down") == 0) { - int prev_interval = actx->authz.interval; - - actx->authz.interval += 5; - if (actx->authz.interval < prev_interval) + if (pg_add_s32_overflow(actx->authz.interval, 5, &actx->authz.interval)) { actx_error(actx, "slow_down interval overflow"); goto token_cleanup; @@ -2974,8 +2972,25 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) * Wait for the required interval before issuing the next * request. */ - if (!set_timer(actx, actx->authz.interval * 1000)) - goto error_return; + { + /* + * Avoid overflow of long int. (By the time we reach + * LONG_MAX milliseconds -- 24 days on 32-bit platforms -- + * continuing to honor slow_down requests seems pretty + * pointless anyway.) + */ + int64 interval_ms; + + if (pg_mul_s64_overflow(actx->authz.interval, 1000, + &interval_ms) + || (interval_ms > LONG_MAX)) + { + interval_ms = LONG_MAX; + } + + if (!set_timer(actx, (long) interval_ms)) + goto error_return; + } /* * No Curl requests are running, so we can simplify by having From 5f67124fa43448702acc0f9448e0ad118fe5e791 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 2 Aug 2026 11:26:30 -0400 Subject: [PATCH 181/250] Update time zone data files to tzdata release 2026c. Alberta (America/Edmonton) moved to permanent UTC-06 on 2026-06-18, which will affect their clocks beginning on 2026-11-01. For lack of any clarity on the point, assume their TZ abbreviation will be CST from that time forward. Morocco (Africa/Casablanca) will move to permanent UTC+00, without daylight saving time transitions, on 2026-09-20. Backpatch-through: 14 --- src/timezone/data/tzdata.zi | 138 +++--------------------------------- 1 file changed, 8 insertions(+), 130 deletions(-) diff --git a/src/timezone/data/tzdata.zi b/src/timezone/data/tzdata.zi index 53082964703..079a47fda43 100644 --- a/src/timezone/data/tzdata.zi +++ b/src/timezone/data/tzdata.zi @@ -1,4 +1,4 @@ -# version 2026b +# version 2026c # redo posix_only # This zic input file is in the public domain. R d 1916 o - Jun 14 23s 1 S @@ -135,132 +135,6 @@ R M 2025 o - F 23 3 -1 - R M 2025 o - Ap 6 2 0 - R M 2026 o - F 15 3 -1 - R M 2026 o - Mar 22 2 0 - -R M 2027 o - F 7 3 -1 - -R M 2027 o - Mar 14 2 0 - -R M 2028 o - Ja 23 3 -1 - -R M 2028 o - Mar 5 2 0 - -R M 2029 o - Ja 14 3 -1 - -R M 2029 o - F 18 2 0 - -R M 2029 o - D 30 3 -1 - -R M 2030 o - F 10 2 0 - -R M 2030 o - D 22 3 -1 - -R M 2031 o - Ja 26 2 0 - -R M 2031 o - D 14 3 -1 - -R M 2032 o - Ja 18 2 0 - -R M 2032 o - N 28 3 -1 - -R M 2033 o - Ja 9 2 0 - -R M 2033 o - N 20 3 -1 - -R M 2033 o - D 25 2 0 - -R M 2034 o - N 5 3 -1 - -R M 2034 o - D 17 2 0 - -R M 2035 o - O 28 3 -1 - -R M 2035 o - D 9 2 0 - -R M 2036 o - O 19 3 -1 - -R M 2036 o - N 23 2 0 - -R M 2037 o - O 4 3 -1 - -R M 2037 o - N 15 2 0 - -R M 2038 o - S 26 3 -1 - -R M 2038 o - O 31 2 0 - -R M 2039 o - S 18 3 -1 - -R M 2039 o - O 23 2 0 - -R M 2040 o - S 2 3 -1 - -R M 2040 o - O 14 2 0 - -R M 2041 o - Au 25 3 -1 - -R M 2041 o - S 29 2 0 - -R M 2042 o - Au 10 3 -1 - -R M 2042 o - S 21 2 0 - -R M 2043 o - Au 2 3 -1 - -R M 2043 o - S 13 2 0 - -R M 2044 o - Jul 24 3 -1 - -R M 2044 o - Au 28 2 0 - -R M 2045 o - Jul 9 3 -1 - -R M 2045 o - Au 20 2 0 - -R M 2046 o - Jul 1 3 -1 - -R M 2046 o - Au 5 2 0 - -R M 2047 o - Jun 23 3 -1 - -R M 2047 o - Jul 28 2 0 - -R M 2048 o - Jun 7 3 -1 - -R M 2048 o - Jul 19 2 0 - -R M 2049 o - May 30 3 -1 - -R M 2049 o - Jul 4 2 0 - -R M 2050 o - May 15 3 -1 - -R M 2050 o - Jun 26 2 0 - -R M 2051 o - May 7 3 -1 - -R M 2051 o - Jun 18 2 0 - -R M 2052 o - Ap 28 3 -1 - -R M 2052 o - Jun 2 2 0 - -R M 2053 o - Ap 13 3 -1 - -R M 2053 o - May 25 2 0 - -R M 2054 o - Ap 5 3 -1 - -R M 2054 o - May 10 2 0 - -R M 2055 o - Mar 28 3 -1 - -R M 2055 o - May 2 2 0 - -R M 2056 o - Mar 12 3 -1 - -R M 2056 o - Ap 23 2 0 - -R M 2057 o - Mar 4 3 -1 - -R M 2057 o - Ap 8 2 0 - -R M 2058 o - F 17 3 -1 - -R M 2058 o - Mar 31 2 0 - -R M 2059 o - F 9 3 -1 - -R M 2059 o - Mar 23 2 0 - -R M 2060 o - F 1 3 -1 - -R M 2060 o - Mar 7 2 0 - -R M 2061 o - Ja 16 3 -1 - -R M 2061 o - F 27 2 0 - -R M 2062 o - Ja 8 3 -1 - -R M 2062 o - F 12 2 0 - -R M 2062 o - D 31 3 -1 - -R M 2063 o - F 4 2 0 - -R M 2063 o - D 16 3 -1 - -R M 2064 o - Ja 27 2 0 - -R M 2064 o - D 7 3 -1 - -R M 2065 o - Ja 11 2 0 - -R M 2065 o - N 22 3 -1 - -R M 2066 o - Ja 3 2 0 - -R M 2066 o - N 14 3 -1 - -R M 2066 o - D 26 2 0 - -R M 2067 o - N 6 3 -1 - -R M 2067 o - D 11 2 0 - -R M 2068 o - O 21 3 -1 - -R M 2068 o - D 2 2 0 - -R M 2069 o - O 13 3 -1 - -R M 2069 o - N 17 2 0 - -R M 2070 o - O 5 3 -1 - -R M 2070 o - N 9 2 0 - -R M 2071 o - S 20 3 -1 - -R M 2071 o - N 1 2 0 - -R M 2072 o - S 11 3 -1 - -R M 2072 o - O 16 2 0 - -R M 2073 o - Au 27 3 -1 - -R M 2073 o - O 8 2 0 - -R M 2074 o - Au 19 3 -1 - -R M 2074 o - S 30 2 0 - -R M 2075 o - Au 11 3 -1 - -R M 2075 o - S 15 2 0 - -R M 2076 o - Jul 26 3 -1 - -R M 2076 o - S 6 2 0 - -R M 2077 o - Jul 18 3 -1 - -R M 2077 o - Au 22 2 0 - -R M 2078 o - Jul 10 3 -1 - -R M 2078 o - Au 14 2 0 - -R M 2079 o - Jun 25 3 -1 - -R M 2079 o - Au 6 2 0 - -R M 2080 o - Jun 16 3 -1 - -R M 2080 o - Jul 21 2 0 - -R M 2081 o - Jun 1 3 -1 - -R M 2081 o - Jul 13 2 0 - -R M 2082 o - May 24 3 -1 - -R M 2082 o - Jun 28 2 0 - -R M 2083 o - May 16 3 -1 - -R M 2083 o - Jun 20 2 0 - -R M 2084 o - Ap 30 3 -1 - -R M 2084 o - Jun 11 2 0 - -R M 2085 o - Ap 22 3 -1 - -R M 2085 o - May 27 2 0 - -R M 2086 o - Ap 14 3 -1 - -R M 2086 o - May 19 2 0 - -R M 2087 o - Mar 30 3 -1 - -R M 2087 o - May 11 2 0 - R NA 1994 o - Mar 21 0 -1 WAT R NA 1994 2017 - S Su>=1 2 0 CAT R NA 1995 2017 - Ap Su>=1 2 -1 WAT @@ -2106,7 +1980,8 @@ Z Africa/Casablanca -0:30:20 - LMT 1913 O 26 0 M %z 1984 Mar 16 1 - %z 1986 0 M %z 2018 O 28 3 -1 M %z +1 M %z 2026 S 20 2 +0 - %z Z Africa/Ceuta -0:21:16 - LMT 1901 Ja 1 0u 0 - WET 1918 May 6 23 0 1 WEST 1918 O 7 23 @@ -2119,7 +1994,8 @@ Z Africa/Ceuta -0:21:16 - LMT 1901 Ja 1 0u Z Africa/El_Aaiun -0:52:48 - LMT 1934 -1 - %z 1976 Ap 14 0 M %z 2018 O 28 3 -1 M %z +1 M %z 2026 S 20 2 +0 - %z Z Africa/Johannesburg 1:52 - LMT 1892 F 8 1:30 - SAST 1903 Mar 2 SA SAST @@ -2483,7 +2359,9 @@ Z America/Detroit -5:32:11 - LMT 1905 -5 u E%sT Z America/Edmonton -7:33:52 - LMT 1906 S -7 Ed M%sT 1987 --7 C M%sT +-7 C M%sT 2026 Jun 18 +-7 1 MDT 2026 N 1 2 +-6 - CST Z America/Eirunepe -4:39:28 - LMT 1914 -5 B %z 1988 S 12 -5 - %z 1993 S 28 From 4689ea9ceee362efe2569cd3b0e5c7072a8a24bc Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 2 Aug 2026 13:22:39 -0400 Subject: [PATCH 182/250] Fix memory-safety bugs in the ispell/hunspell dictionary loader. Allocate CompoundAffix with room for its terminator, initialize the old-format flag buffer before NIAddAffix(), and reject incomplete or missing Hunspell AF aliases. None of these errors would be likely to trigger on real dictionary files, accounting for the lack of previous reports; but they're certainly bugs. Bug: #19595 Reported-by: Michael Malis Author: Andrey Rachitskiy Reviewed-by: Tom Lane Discussion: https://postgr.es/m/19595-7dc18b4e212c4757@postgresql.org Backpatch-through: 14 --- src/backend/tsearch/spell.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c index 7f8c677a438..e4393e35971 100644 --- a/src/backend/tsearch/spell.c +++ b/src/backend/tsearch/spell.c @@ -1182,12 +1182,18 @@ getAffixFlagSet(IspellDict *Conf, char *s) errmsg("invalid affix alias \"%s\"", s))); if (curaffix > 0 && curaffix < Conf->nAffixData) + { + if (Conf->AffixData[curaffix] == NULL) + ereport(ERROR, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid affix alias \"%s\"", s))); /* * Do not subtract 1 from curaffix because empty string was added * in NIImportOOAffixes */ return Conf->AffixData[curaffix]; + } else if (curaffix > Conf->nAffixData) ereport(ERROR, (errcode(ERRCODE_CONFIG_FILE_ERROR), @@ -1422,6 +1428,13 @@ NIImportOOAffixes(IspellDict *Conf, const char *filename) tsearch_readline_end(&trst); if (ptype) pfree(ptype); + + /* Reject incomplete AF alias table. */ + if (Conf->useFlagAliases && curaffix != naffix) + ereport(ERROR, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("number of aliases is less than specified number %d", + naffix - 1))); } /* @@ -1449,6 +1462,8 @@ NIImportAffixes(IspellDict *Conf, const char *filename) bool oldformat = false; char *recoded = NULL; + flag[0] = '\0'; /* no flag seen yet */ + if (!tsearch_readline_begin(&trst, filename)) ereport(ERROR, (errcode(ERRCODE_CONFIG_FILE_ERROR), @@ -1998,7 +2013,8 @@ NISortAffixes(IspellDict *Conf) /* Store compound affixes in the Conf->CompoundAffix array */ if (Conf->naffixes > 1) qsort(Conf->Affix, Conf->naffixes, sizeof(AFFIX), cmpaffix); - Conf->CompoundAffix = ptr = (CMPDAffix *) palloc(sizeof(CMPDAffix) * Conf->naffixes); + /* +1 for terminator */ + Conf->CompoundAffix = ptr = palloc_array(CMPDAffix, Conf->naffixes + 1); ptr->affix = NULL; for (i = 0; i < Conf->naffixes; i++) From 81b1e79166a524b95628f8d459571f1b029fe19e Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 2 Aug 2026 16:49:18 -0400 Subject: [PATCH 183/250] Tighten up TS dictionary cache entry creation. In the not-too-likely scenario where we successfully created a hash table entry for a TS dictionary, but then failed to make a small memory context for it, we left the hash entry in existence but with a garbage value for dictCtx. This confused the code the next time through, leading to a crash. Rearrange things so that we leave the hash entry in a well-defined state with dictCtx == NULL, and then the next try knows it still needs to make a memory context. Reported-by: Alexander Lakhin Author: Tom Lane Discussion: https://postgr.es/m/0f3ddeb5-0dbd-479c-9d0e-ae254758e624@gmail.com Backpatch-through: 14 --- src/backend/utils/cache/ts_cache.c | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/backend/utils/cache/ts_cache.c b/src/backend/utils/cache/ts_cache.c index 18cccd778fd..eec244fd16b 100644 --- a/src/backend/utils/cache/ts_cache.c +++ b/src/backend/utils/cache/ts_cache.c @@ -279,37 +279,50 @@ lookup_ts_dictionary_cache(Oid dictId) elog(ERROR, "text search template %u has no lexize method", template->tmpllexize); + /* + * OK, create or clear out the hashtable entry + */ if (entry == NULL) { bool found; - /* Now make the cache entry */ entry = (TSDictionaryCacheEntry *) hash_search(TSDictionaryCacheHash, &dictId, HASH_ENTER, &found); Assert(!found); /* it wasn't there a moment ago */ - /* Create private memory context the first time through */ + memset(entry, 0, sizeof(TSDictionaryCacheEntry)); + entry->dictId = dictId; + saveCtx = NULL; + } + else + { + saveCtx = entry->dictCtx; /* could be NULL if we failed before */ + memset(entry, 0, sizeof(TSDictionaryCacheEntry)); + entry->dictId = dictId; + entry->dictCtx = saveCtx; + } + + /* + * Create or clear the entry's private memory context + */ + if (saveCtx == NULL) + { saveCtx = AllocSetContextCreate(CacheMemoryContext, "TS dictionary", ALLOCSET_SMALL_SIZES); + entry->dictCtx = saveCtx; MemoryContextCopyAndSetIdentifier(saveCtx, NameStr(dict->dictname)); } else { - /* Clear the existing entry's private context */ - saveCtx = entry->dictCtx; /* Don't let context's ident pointer dangle while we reset it */ MemoryContextSetIdentifier(saveCtx, NULL); MemoryContextReset(saveCtx); MemoryContextCopyAndSetIdentifier(saveCtx, NameStr(dict->dictname)); } - MemSet(entry, 0, sizeof(TSDictionaryCacheEntry)); - entry->dictId = dictId; - entry->dictCtx = saveCtx; - entry->lexizeOid = template->tmpllexize; if (OidIsValid(template->tmplinit)) From d04a24d9ead0d47f772d5a416813259af5a36e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Mon, 3 Aug 2026 13:52:41 +0200 Subject: [PATCH 184/250] Remove unused arg and dead code in set_attnotnull() The is_valid parameter was never referenced in the function body, and the 'thisatt' local variable is set but never used. Remove both. Oversight in a379061a22a8. Author: Sami Imseih Backpatch-through: 18 Discussion: https://postgr.es/m/CAA5RZ0tHnvSrfUy4jWJchjvkL_aJe0hCnZpMsFRdLrSxCne5qQ@mail.gmail.com --- src/backend/commands/tablecmds.c | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 5abe02615a1..d4eaf30815b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -501,7 +501,7 @@ static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid) static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse, LOCKMODE lockmode); static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, - bool is_valid, bool queue_validation); + bool queue_validation); static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName, bool recurse, bool recursing, @@ -1354,7 +1354,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints, old_notnulls, connames); foreach_int(attrnum, nncols) - set_attnotnull(NULL, rel, attrnum, true, false); + set_attnotnull(NULL, rel, attrnum, false); ObjectAddressSet(address, RelationRelationId, relationId); @@ -7838,10 +7838,9 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse, */ static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, - bool is_valid, bool queue_validation) + bool queue_validation) { Form_pg_attribute attr; - CompactAttribute *thisatt; Assert(!queue_validation || wqueue); @@ -7867,9 +7866,6 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, elog(ERROR, "cache lookup failed for attribute %d of relation %u", attnum, RelationGetRelid(rel)); - thisatt = TupleDescCompactAttr(RelationGetDescr(rel), attnum - 1); - thisatt->attnullability = ATTNULLABLE_VALID; - attr = (Form_pg_attribute) GETSTRUCT(tuple); attr->attnotnull = true; @@ -8051,7 +8047,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName, ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); /* Mark pg_attribute.attnotnull for the column and queue validation */ - set_attnotnull(wqueue, rel, attnum, true, true); + set_attnotnull(wqueue, rel, attnum, true); InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), attnum); @@ -9977,7 +9973,6 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, */ if (constr->contype == CONSTR_NOTNULL) set_attnotnull(wqueue, rel, ccon->attnum, - !constr->skip_validation, !constr->skip_validation); ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); @@ -13300,7 +13295,7 @@ QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel, } /* Set attnotnull appropriately without queueing another validation */ - set_attnotnull(NULL, rel, attnum, true, false); + set_attnotnull(NULL, rel, attnum, false); tab = ATGetQueueEntry(wqueue, rel); tab->verify_new_notnull = true; From 9a8c9338377c397714bb2cc44b46a430dce5bdfe Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Mon, 3 Aug 2026 11:34:58 -0700 Subject: [PATCH 185/250] Do not log subscription conninfo. Logging connection information, even at DEBUG1, creates unnecessary risks. Remove the entire log message because it had no other useful content. Addresses finding 14 in report from linked discussion. Reported-by: Noah Misch Discussion: https://postgr.es/m/20260710195902.4f.noahmisch@microsoft.com Backpatch-through: 14 --- src/backend/replication/logical/worker.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 033d00e6d61..bb40dbb45f7 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -4815,10 +4815,6 @@ SetupApplyOrSyncWorker(int worker_slot) InitializeLogRepWorker(); - /* Connect to the origin and start the replication. */ - elog(DEBUG1, "connecting to publisher using connection string \"%s\"", - MySubscription->conninfo); - /* * Setup callback for syscache so that we know when something changes in * the subscription relation state. From a0daa0b4127dffe99a92c6606ad778f89ec3239f Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Mon, 3 Aug 2026 12:21:05 -0700 Subject: [PATCH 186/250] Fix lock release for role membership grants in DROP OWNED BY. Commit 6566133c5f5 added a case for AuthMemRelationId in AcquireDeletionLock(), but not ReleaseDeletionLock(). The fall-through case would go to UnlockDatabaseObject(), which would raise a WARNING; and the lock would be retained until the end of the transaction. Add the missing branch. Discussion: https://postgr.es/m/2487ddcd737d4fc8e408e87aa9ad4365eed3bbb3.camel@j-davis.com Backpatch-through: 16 --- src/backend/catalog/dependency.c | 3 ++ .../isolation/expected/drop-owned-grant.out | 8 +++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/drop-owned-grant.spec | 30 +++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 src/test/isolation/expected/drop-owned-grant.out create mode 100644 src/test/isolation/specs/drop-owned-grant.spec diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 18316a3968b..49d7cc10037 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1529,6 +1529,9 @@ ReleaseDeletionLock(const ObjectAddress *object) { if (object->classId == RelationRelationId) UnlockRelationOid(object->objectId, AccessExclusiveLock); + else if (object->classId == AuthMemRelationId) + UnlockSharedObject(object->classId, object->objectId, 0, + AccessExclusiveLock); else /* assume we should lock the whole object not a sub-object */ UnlockDatabaseObject(object->classId, object->objectId, 0, diff --git a/src/test/isolation/expected/drop-owned-grant.out b/src/test/isolation/expected/drop-owned-grant.out new file mode 100644 index 00000000000..ea6cca277b6 --- /dev/null +++ b/src/test/isolation/expected/drop-owned-grant.out @@ -0,0 +1,8 @@ +Parsed test spec with 2 sessions + +starting permutation: s1b s1d s2d s1c +step s1b: BEGIN; +step s1d: DROP OWNED BY regress_dropowned_grantor; +step s2d: DROP OWNED BY regress_dropowned_grantor; +step s1c: COMMIT; +step s2d: <... completed> diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 9741b881f02..91d3bebcd1a 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -119,3 +119,4 @@ test: matview-write-skew test: lock-nowait test: ddl-dependency-locking test: pub-concurrent-drop +test: drop-owned-grant diff --git a/src/test/isolation/specs/drop-owned-grant.spec b/src/test/isolation/specs/drop-owned-grant.spec new file mode 100644 index 00000000000..636cc213a6b --- /dev/null +++ b/src/test/isolation/specs/drop-owned-grant.spec @@ -0,0 +1,30 @@ +# Test locking of role membership grants during concurrent DROP OWNED BY. + +setup +{ + CREATE ROLE regress_dropowned_role; + CREATE ROLE regress_dropowned_member; + CREATE ROLE regress_dropowned_grantor; + GRANT regress_dropowned_role TO regress_dropowned_grantor + WITH ADMIN OPTION; + SET ROLE regress_dropowned_grantor; + GRANT regress_dropowned_role TO regress_dropowned_member; + RESET ROLE; +} + +teardown +{ + DROP ROLE regress_dropowned_member; + DROP ROLE regress_dropowned_grantor; + DROP ROLE regress_dropowned_role; +} + +session s1 +step s1b { BEGIN; } +step s1d { DROP OWNED BY regress_dropowned_grantor; } +step s1c { COMMIT; } + +session s2 +step s2d { DROP OWNED BY regress_dropowned_grantor; } + +permutation s1b s1d s2d s1c From bf048f0e17a1ea82dc22c8ad446d14670b52451c Mon Sep 17 00:00:00 2001 From: David Rowley Date: Tue, 4 Aug 2026 16:09:55 +1200 Subject: [PATCH 187/250] Fix missing MCXT_ALLOC_NO_OOM handling in MemoryContextAllocAligned Fix missing NULL check in MemoryContextAllocAligned(). The underlying call to MemoryContextAllocExtended() could return NULL when flags contains MCXT_ALLOC_NO_OOM and the underlying malloc fails. There are no current callers using MemoryContextAllocAligned() that pass the MCXT_ALLOC_NO_OOM in core, so no live bug fix in core here. However, an extension might use this pattern, so we'd better fix. Fix this so we correctly pass the NULL to the caller rather than trying to write to a NULL memory address. This also fixes the same bug in AlignedAllocRealloc(), which is also unused in core. Backpatch to v16, where these functions first appeared. Author: Chao Li Discussion: https://postgr.es/m/07DAC4C3-120D-4F3C-8FEE-BA236F7E9C1D@gmail.com Backpatch-through: 16 --- src/backend/utils/mmgr/mcxt.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 85ee9ad9029..3654b61e68d 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1486,6 +1486,13 @@ MemoryContextAllocAligned(MemoryContext context, /* perform the actual allocation */ unaligned = MemoryContextAllocExtended(context, alloc_size, flags); + if (unlikely(unaligned == NULL)) + { + /* NULL can be returned only when using MCXT_ALLOC_NO_OOM */ + Assert(flags & MCXT_ALLOC_NO_OOM); + return NULL; + } + /* set the aligned pointer */ aligned = (void *) TYPEALIGN(alignto, (char *) unaligned + sizeof(MemoryChunk)); From 6298a41b4e34773dbc0c50c26de825a4efe31f07 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Tue, 4 Aug 2026 18:00:27 +1200 Subject: [PATCH 188/250] Fix missing money overflow checks for INT64_MIN / -1 Similar to what 1f7cb5c30 did for the INT types, protect against overflow when dividing the lowest possible money value by -1. This cannot be represented on a two's complement machine. Without this check, the result depends on the machine, and in the worst case, could result in a crash. With the fix installed, this will now result in: ERROR: money out of range Bug: #19585 Author: Andrey Rachitskiy Reported-by: Michael Malis Reviewed-by: Tristan Partin Reviewed-by: Rafia Sabih Discussion: https://postgr.es/m/19586-bb603bf5ad9934dd%40postgresql.org Discussion: https://postgr.es/m/CAB8bMisnXJVXte6s3kUOpuuAY9%3D9kehG6MMX-%2BTQoFsSGan22Q%40mail.gmail.com Backpatch-through: 14 --- src/backend/utils/adt/cash.c | 16 ++++++++++++++++ src/test/regress/expected/money.out | 8 ++++++++ src/test/regress/sql/money.sql | 4 ++++ 3 files changed, 28 insertions(+) diff --git a/src/backend/utils/adt/cash.c b/src/backend/utils/adt/cash.c index 611d23f3cb0..935ae3c2ebf 100644 --- a/src/backend/utils/adt/cash.c +++ b/src/backend/utils/adt/cash.c @@ -160,6 +160,22 @@ cash_div_int64(Cash c, int64 i) (errcode(ERRCODE_DIVISION_BY_ZERO), errmsg("division by zero"))); + /* + * INT64_MIN / -1 is problematic, since the result can't be represented on + * a two's-complement machine. Some machines produce INT64_MIN, some + * produce zero, some throw an exception. We can dodge the problem by + * recognizing that division by -1 is the same as negation. + */ + if (i == -1) + { + if (unlikely(c == PG_INT64_MIN)) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("money out of range"))); + return -c; + } + + /* No overflow is possible */ return c / i; } diff --git a/src/test/regress/expected/money.out b/src/test/regress/expected/money.out index cc2ff4d96e8..e7fdb9b7d0d 100644 --- a/src/test/regress/expected/money.out +++ b/src/test/regress/expected/money.out @@ -539,6 +539,14 @@ SELECT '-1'::money / 1.175494e-38::float4; ERROR: money out of range SELECT '92233720368547758.07'::money * 2::int4; ERROR: money out of range +SELECT '-92233720368547758.08'::money * -1::int8; +ERROR: money out of range +SELECT '-92233720368547758.08'::money / -1::int8; +ERROR: money out of range +SELECT '-92233720368547758.08'::money / -1::int4; +ERROR: money out of range +SELECT '-92233720368547758.08'::money / -1::int2; +ERROR: money out of range SELECT '1'::money / 0::int2; ERROR: division by zero SELECT '42'::money * 'inf'::float8; diff --git a/src/test/regress/sql/money.sql b/src/test/regress/sql/money.sql index b888ec21c30..d769a211090 100644 --- a/src/test/regress/sql/money.sql +++ b/src/test/regress/sql/money.sql @@ -142,6 +142,10 @@ SELECT '-92233720368547758.08'::money - '0.01'::money; SELECT '92233720368547758.07'::money * 2::float8; SELECT '-1'::money / 1.175494e-38::float4; SELECT '92233720368547758.07'::money * 2::int4; +SELECT '-92233720368547758.08'::money * -1::int8; +SELECT '-92233720368547758.08'::money / -1::int8; +SELECT '-92233720368547758.08'::money / -1::int4; +SELECT '-92233720368547758.08'::money / -1::int2; SELECT '1'::money / 0::int2; SELECT '42'::money * 'inf'::float8; SELECT '42'::money * '-inf'::float8; From c374f2807c236dd8fb4bee9f4ebfe4af300491f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Tue, 4 Aug 2026 09:06:46 +0200 Subject: [PATCH 189/250] Fix ALTER COLUMN ... DROP EXPRESSION with subpartitions Per commit 8bf6ec3ba3a4, a column can be GENERATED only if it is such in the whole inheritance tree. For this reason, ATPrepDropExpression refuses to be called with ONLY on a partitioned table. To detect this, the current implementation checks whether recurse is set to false and the rel has direct children. Recursion is implemented with ATSimpleRecursion, which calls ATPrepCmd with recurse = false for every node in the tree. Inner nodes (for example a partition which itself has subpartitions) then fail the check, accidentally preventing the command from working on inheritance trees of depth > 2. This commit fixes it by also checking that we're at the top level of the recursive calls using the recursing parameter, which is always true when called through ATSimpleRecursion, always false when invoked on the root rel. Also, remove a comment claiming that DROP EXPRESSION could be implemented with some effort. It cannot, as the commit message for 8bf6ec3ba3a4 explains. Author: Alberto Piai Backpatch-through: 14 Discussion: https://postgr.es/m/DHMT78XOD8BK.341V3H87KZ7NO@gmail.com --- src/backend/commands/tablecmds.c | 15 ++---- .../regress/expected/generated_stored.out | 50 +++++++++++++++++++ src/test/regress/sql/generated_stored.sql | 15 ++++++ 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index d4eaf30815b..1ef8b355116 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -8752,17 +8752,12 @@ static void ATPrepDropExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode) { /* - * Reject ONLY if there are child tables. We could implement this, but it - * is a bit complicated. GENERATED clauses must be attached to the column - * definition and cannot be added later like DEFAULT, so if a child table - * has a generation expression that the parent does not have, the child - * column will necessarily be an attislocal column. So to implement ONLY - * here, we'd need extra code to update attislocal of the direct child - * tables, somewhat similar to how DROP COLUMN does it, so that the - * resulting state can be properly dumped and restored. + * Reject ONLY if there are child tables -- but only, of course, at the + * top of the tree, otherwise it'd be impossible to run this command with + * trees deeper than two levels. Caller already got lock. */ - if (!recurse && - find_inheritance_children(RelationGetRelid(rel), lockmode)) + if (!recurse && !recursing && + find_inheritance_children(RelationGetRelid(rel), NoLock)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("ALTER TABLE / DROP EXPRESSION must be applied to child tables too"))); diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out index 86a04548a02..77fcd9002c6 100644 --- a/src/test/regress/expected/generated_stored.out +++ b/src/test/regress/expected/generated_stored.out @@ -1329,6 +1329,56 @@ Inherits: gtest30 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION; -- error ERROR: cannot drop generation expression from inherited column +BEGIN; +CREATE TABLE gtest30_1_1 () INHERITS (gtest30_1); +ALTER TABLE gtest30 ALTER COLUMN b DROP EXPRESSION; +\d gtest30_1_1 + Table "generated_stored_tests.gtest30_1_1" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | | + b | integer | | | +Inherits: gtest30_1 + +ROLLBACK; +-- test drop expression with subpartitions +CREATE TABLE gtest_root (a int, b int, c int GENERATED ALWAYS AS (a + b) STORED) PARTITION BY LIST (a); +CREATE TABLE gtest_node PARTITION OF gtest_root FOR VALUES IN (1) PARTITION BY LIST (b); +CREATE TABLE gtest_leaf PARTITION OF gtest_node FOR VALUES IN (1); +ALTER TABLE gtest_node ALTER COLUMN c DROP EXPRESSION; -- fails +ERROR: cannot drop generation expression from inherited column +ALTER TABLE ONLY gtest_root ALTER COLUMN c DROP EXPRESSION; -- fails +ERROR: ALTER TABLE / DROP EXPRESSION must be applied to child tables too +ALTER TABLE gtest_root ALTER COLUMN c DROP EXPRESSION; +\d gtest_(root|node|leaf) + Table "generated_stored_tests.gtest_leaf" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | | + b | integer | | | + c | integer | | | +Partition of: gtest_node FOR VALUES IN (1) + +Partitioned table "generated_stored_tests.gtest_node" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | | + b | integer | | | + c | integer | | | +Partition of: gtest_root FOR VALUES IN (1) +Partition key: LIST (b) +Number of partitions: 1 (Use \d+ to list them.) + +Partitioned table "generated_stored_tests.gtest_root" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | | + b | integer | | | + c | integer | | | +Partition key: LIST (a) +Number of partitions: 1 (Use \d+ to list them.) + +DROP TABLE gtest_root; -- composite type dependencies CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text); CREATE TABLE gtest31_2 (x int, y gtest31_1); diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql index 6abae289ffe..9ba83c6f5ab 100644 --- a/src/test/regress/sql/generated_stored.sql +++ b/src/test/regress/sql/generated_stored.sql @@ -597,6 +597,21 @@ ALTER TABLE ONLY gtest30 ALTER COLUMN b DROP EXPRESSION; -- error \d gtest30 \d gtest30_1 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION; -- error +BEGIN; +CREATE TABLE gtest30_1_1 () INHERITS (gtest30_1); +ALTER TABLE gtest30 ALTER COLUMN b DROP EXPRESSION; +\d gtest30_1_1 +ROLLBACK; + +-- test drop expression with subpartitions +CREATE TABLE gtest_root (a int, b int, c int GENERATED ALWAYS AS (a + b) STORED) PARTITION BY LIST (a); +CREATE TABLE gtest_node PARTITION OF gtest_root FOR VALUES IN (1) PARTITION BY LIST (b); +CREATE TABLE gtest_leaf PARTITION OF gtest_node FOR VALUES IN (1); +ALTER TABLE gtest_node ALTER COLUMN c DROP EXPRESSION; -- fails +ALTER TABLE ONLY gtest_root ALTER COLUMN c DROP EXPRESSION; -- fails +ALTER TABLE gtest_root ALTER COLUMN c DROP EXPRESSION; +\d gtest_(root|node|leaf) +DROP TABLE gtest_root; -- composite type dependencies CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text); From 6b46a5d1b6164f2fdb15229dea9f3ed94ef20760 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Tue, 4 Aug 2026 17:03:27 +0900 Subject: [PATCH 190/250] Fix error handling in getCopyDataMessage() and pqFunctionCall3() Commit f6f0542266f0 changed getNotify(), getParameterStatus(), and related libpq message-processing paths to abandon the connection on out-of-memory errors. However, getCopyDataMessage() and pqFunctionCall3() did not handle this new fatal-error state. Both can process asynchronous NotificationResponse and ParameterStatus messages while waiting for other responses. If one of those messages triggered a fatal error, these loops continued processing instead of reporting it immediately. Fix this by checking for a saved fatal error after processing an asynchronous message. If the connection has been abandoned, return the appropriate error immediately instead of continuing to parse input. Backpatch to v18, where commit f6f0542266f0 introduced this issue. Author: Anthonin Bonnefoy Reviewed-by: Ewan Young Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CAO6_XqpGfm+XHE1OzS=_+jroeDOxhhGa11P3cbm9q2gT05yorA@mail.gmail.com Backpatch-through: 18 --- src/interfaces/libpq/fe-protocol3.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 048851a3635..0b9f111f41e 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -1881,6 +1881,13 @@ getCopyDataMessage(PGconn *conn) return -1; } + /* + * An error may have been triggered while processing the message, + * report it if it's the case + */ + if (conn->error_result && conn->status == CONNECTION_BAD) + return -2; + /* Drop the processed message and loop around for another */ pqParseDone(conn, conn->inCursor); } @@ -2375,6 +2382,13 @@ pqFunctionCall3(PGconn *conn, Oid fnid, return pqPrepareAsyncResult(conn); } + /* + * An error may have been triggered while processing the message, bail + * out + */ + if (conn->error_result && conn->status == CONNECTION_BAD) + return pqPrepareAsyncResult(conn); + /* Completed parsing this message, keep going */ pqParseDone(conn, conn->inStart + 5 + msgLength); needInput = false; From 19f0391df48202e2c6ac2e30a4c1c284aab5681b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Tue, 4 Aug 2026 11:44:11 +0200 Subject: [PATCH 191/250] pg_surgery: Fix infinite loop on large TID arrays heap_force_common() tracked the current position in the caller-supplied tid[] using OffsetNumber, which is only 16 bits wide, so when the array held more than 65535 entries, the updated index wrapped around and the outer loop never reached the exit condition. A SQL call with a sufficiently large TID array would then run until interrupted. Fix by tracking the tid[] position using int instead of OffsetNumber. A regress case based on the report is included. Author: Andrey Rachitskiy Reviewed-by: Andrey Borodin Reported-by: Yuelin Wang <1217816127@qq.com> Backpatch-through: 14 Bug: #19607 Discussion: https://postgr.es/m/19607-2f256a66481c514b@postgresql.org --- contrib/pg_surgery/expected/heap_surgery.out | 17 +++++++++++++++++ contrib/pg_surgery/heap_surgery.c | 6 +++--- contrib/pg_surgery/sql/heap_surgery.sql | 8 ++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/contrib/pg_surgery/expected/heap_surgery.out b/contrib/pg_surgery/expected/heap_surgery.out index df7d13b0908..42586137d88 100644 --- a/contrib/pg_surgery/expected/heap_surgery.out +++ b/contrib/pg_surgery/expected/heap_surgery.out @@ -134,6 +134,23 @@ select heap_force_kill('htab2'::regclass, ARRAY['(0, 3)']::tid[]); (1 row) +-- a tid[] larger than 65535 entries must still finish +create temp table htab3(a int); +insert into htab3 values (1); +select heap_force_kill( + 'htab3'::regclass, + array(select '(0,1)'::tid from generate_series(1, 65536))); + heap_force_kill +----------------- + +(1 row) + +select count(*) from htab3; + count +------- + 0 +(1 row) + -- materialized view. -- note that we don't commit the transaction, so autovacuum can't interfere. begin; diff --git a/contrib/pg_surgery/heap_surgery.c b/contrib/pg_surgery/heap_surgery.c index 6a38ac577c6..337fe92e615 100644 --- a/contrib/pg_surgery/heap_surgery.c +++ b/contrib/pg_surgery/heap_surgery.c @@ -44,7 +44,7 @@ static Datum heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt); static void sanity_check_tid_array(ArrayType *ta, int *ntids); static BlockNumber find_tids_one_page(ItemPointer tids, int ntids, - OffsetNumber *next_start_ptr); + int *next_start_ptr); /*------------------------------------------------------------------------- * heap_force_kill() @@ -91,7 +91,7 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) int ntids, nblocks; Relation rel; - OffsetNumber curr_start_ptr, + int curr_start_ptr, next_start_ptr; bool include_this_tid[MaxHeapTuplesPerPage]; @@ -413,7 +413,7 @@ sanity_check_tid_array(ArrayType *ta, int *ntids) * ------------------------------------------------------------------------ */ static BlockNumber -find_tids_one_page(ItemPointer tids, int ntids, OffsetNumber *next_start_ptr) +find_tids_one_page(ItemPointer tids, int ntids, int *next_start_ptr) { int i; BlockNumber prev_blkno, diff --git a/contrib/pg_surgery/sql/heap_surgery.sql b/contrib/pg_surgery/sql/heap_surgery.sql index 6526b27535d..c4e933da13a 100644 --- a/contrib/pg_surgery/sql/heap_surgery.sql +++ b/contrib/pg_surgery/sql/heap_surgery.sql @@ -65,6 +65,14 @@ select heap_force_kill('htab2'::regclass, ARRAY[NULL]::tid[]); -- but we should be able to kill the one tuple we have select heap_force_kill('htab2'::regclass, ARRAY['(0, 3)']::tid[]); +-- a tid[] larger than 65535 entries must still finish +create temp table htab3(a int); +insert into htab3 values (1); +select heap_force_kill( + 'htab3'::regclass, + array(select '(0,1)'::tid from generate_series(1, 65536))); +select count(*) from htab3; + -- materialized view. -- note that we don't commit the transaction, so autovacuum can't interfere. begin; From 13a9be148e530b9b5c0af2b499e3a08c1f5eb053 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Wed, 5 Aug 2026 11:36:43 +0900 Subject: [PATCH 192/250] doc: Update XID wraparound error example Commit edee0c621de, and the equivalent v17 commit f2353dd71724, changed the runtime XID wraparound messages to use "transaction IDs" terminology, but one corresponding error example in maintenance.sgml still used the older XID wording. Update that documentation example in line with the current runtime message. Backpatch to v17, where the runtime messages were changed. Author: Fujii Masao Reviewed-by: Yugo Nagata Discussion: https://postgr.es/m/CAHGQGwHTN-Xc5iDtbzNSjfxuab5Y9qAArw8cB4PrrDJpZ+1fgA@mail.gmail.com Backpatch-through: 17 --- doc/src/sgml/maintenance.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 282199ca033..80e0024b062 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -685,7 +685,7 @@ HINT: To avoid XID assignment failures, execute a database-wide VACUUM in that there are fewer than three million transactions left until wraparound: -ERROR: database is not accepting commands that assign new XIDs to avoid wraparound data loss in database "mydb" +ERROR: database is not accepting commands that assign new transaction IDs to avoid wraparound data loss in database "mydb" HINT: Execute a database-wide VACUUM in that database. From 011384ba45fe193131f92c2d16c45f20d909301d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Wed, 5 Aug 2026 11:40:39 +0200 Subject: [PATCH 193/250] Fix calculating length of match to localized month/weekday names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seq_search_localized() returns the length of the matching prefix in *len, but because it internally case-folds the inputs, it gets confused on the length. The caller expects to get the length of the prefix in the original string, but what it actually returns is the length of the prefix after case-folding, which can be different if the case-folded characters have different byte-length than the original, or with ICU, if the case-folding changes the number of characters (e.g. "ß", the German double s). To fix, once we have determined that we have a match, work harder to find the match's length in the original string. This adds some overhead, but the strings are expected to be short. The function does "case-folding" by converting a string to upper-case, then to lower-case, which is a little ugly given that we have dedicated functions for case-folding nowadays. But switching to that doesn't seem appropriate to backpatch in a security fix, and that's not available in older stable versions, anyway. Author: Heikki Linnakangas Reported-by: Xint Code Reviewed-by: Jeff Davis --- src/backend/utils/adt/formatting.c | 163 +++++++++++++++--- .../regress/expected/collate.linux.utf8.out | 18 ++ src/test/regress/sql/collate.linux.utf8.sql | 3 + 3 files changed, 158 insertions(+), 26 deletions(-) diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index 295e55ccd8b..6cf68792c4a 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -114,6 +114,7 @@ #define DCH_MAX_ITEM_SIZ 12 /* max localized day name */ #define NUM_MAX_ITEM_SIZ 8 /* roman number (RN has 15 chars) */ +#define MAX_L10N_DATA 80 /* max localized day or month name */ /* ---------- * Format parser structs @@ -1097,8 +1098,8 @@ static int from_char_parse_int_len(int *dest, const char **src, const int len, FormatNode *node, Node *escontext); static int from_char_parse_int(int *dest, const char **src, FormatNode *node, Node *escontext); -static int seq_search_ascii(const char *name, const char *const *array, int *len); -static int seq_search_localized(const char *name, char **array, int *len, +static int seq_search_ascii(const char *name, const char *const *array, size_t *len); +static int seq_search_localized(const char *name, char **array, size_t *len, Oid collid); static bool from_char_seq_search(int *dest, const char **src, const char *const *array, @@ -2317,7 +2318,7 @@ from_char_parse_int(int *dest, const char **src, FormatNode *node, * suitable for comparisons to ASCII strings. */ static int -seq_search_ascii(const char *name, const char *const *array, int *len) +seq_search_ascii(const char *name, const char *const *array, size_t *len) { unsigned char firstc; const char *const *a; @@ -2362,6 +2363,41 @@ seq_search_ascii(const char *name, const char *const *array, int *len) return -1; } +/* + * Compare 'name' with 'element' in a case-insensitive way, by first + * converting 'name' to upper case, then lower case. ('element' is already + * case-folded that way.) + * + * A helper function for seq_search_localized(). + */ +static bool +casefold_str_cmp(const char *name, size_t name_len, + const char *element, size_t element_len, + pg_locale_t mylocale) +{ + /* + * 'name' is expected to fit in MAX_L10N_DATA, even with the case + * conversions. + */ + char upper_substr[MAX_L10N_DATA]; + size_t upper_substr_len; + char lower_substr[MAX_L10N_DATA]; + size_t lower_substr_len; + + upper_substr_len = pg_strupper(upper_substr, sizeof(upper_substr), + name, name_len, + mylocale); + if (upper_substr_len > sizeof(upper_substr) - 1) + return false; /* shouldn't happen */ + lower_substr_len = pg_strlower(lower_substr, sizeof(lower_substr), + upper_substr, upper_substr_len, + mylocale); + if (lower_substr_len > sizeof(lower_substr) - 1) + return false; /* shouldn't happen */ + + return strcmp(lower_substr, element) == 0; +} + /* * Sequentially search an array of possibly non-English words for * a case-insensitive match to the initial character(s) of "name". @@ -2374,11 +2410,13 @@ seq_search_ascii(const char *name, const char *const *array, int *len) * the arrays exported by pg_locale.c aren't const. */ static int -seq_search_localized(const char *name, char **array, int *len, Oid collid) +seq_search_localized(const char *name, char **array, size_t *len, Oid collid) { - char **a; + size_t name_len = strlen(name); + const char *name_end = name + name_len; char *upper_name; char *lower_name; + pg_locale_t mylocale; *len = 0; @@ -2390,9 +2428,9 @@ seq_search_localized(const char *name, char **array, int *len, Oid collid) * The case-folding processing done below is fairly expensive, so before * doing that, make a quick pass to see if there is an exact match. */ - for (a = array; *a != NULL; a++) + for (char **a = array; *a != NULL; a++) { - int element_len = strlen(*a); + size_t element_len = strlen(*a); if (strncmp(name, *a, element_len) == 0) { @@ -2401,36 +2439,109 @@ seq_search_localized(const char *name, char **array, int *len, Oid collid) } } + mylocale = pg_newlocale_from_collation(collid); + /* * Fold to upper case, then to lower case, so that we can match reliably * even in languages in which case conversions are not injective. */ - upper_name = str_toupper(name, strlen(name), collid); + upper_name = str_toupper(name, name_len, collid); lower_name = str_tolower(upper_name, strlen(upper_name), collid); pfree(upper_name); - for (a = array; *a != NULL; a++) + for (char **a = array; *a != NULL; a++) { - char *upper_element; - char *lower_element; - int element_len; + char upper_element[MAX_L10N_DATA]; + size_t upper_element_len; + char lower_element[MAX_L10N_DATA]; + size_t lower_element_len; /* Likewise upper/lower-case array element */ - upper_element = str_toupper(*a, strlen(*a), collid); - lower_element = str_tolower(upper_element, strlen(upper_element), - collid); - pfree(upper_element); - element_len = strlen(lower_element); - - /* Match? */ - if (strncmp(lower_name, lower_element, element_len) == 0) + upper_element_len = pg_strupper(upper_element, sizeof(upper_element), + *a, strlen(*a), + mylocale); + if (upper_element_len > sizeof(upper_element) - 1) + continue; /* shouldn't happen */ + lower_element_len = pg_strlower(lower_element, sizeof(lower_element), + upper_element, upper_element_len, + mylocale); + if (lower_element_len > sizeof(lower_element) - 1) + continue; /* shouldn't happen */ + + /* Is 'lower_element' a prefix of 'lower_name' ? */ + if (strncmp(lower_name, lower_element, lower_element_len) == 0) { - *len = element_len; - pfree(lower_element); - pfree(lower_name); - return a - array; + /* + * We have a match, but we still need to figure out how long the + * match is. The case conversions could have changed the lengths + * of either string, or both. + */ + const char *ep; + const char *element_end; + size_t element_nchars; + size_t substr_len; + size_t substr_nchars; + + /* + * First, check the easy case that the string matches as whole. + */ + if (strlen(lower_name) == lower_element_len) + { + *len = name_len; + pfree(lower_name); + return a - array; + } + + /* + * Another good guess is that the case conversions did not change + * the number of characters. + */ + + /* count characters in the element */ + ep = lower_element; + element_end = lower_element + lower_element_len; + for (element_nchars = 0; ep < element_end; element_nchars++) + ep += pg_mblen_range(ep, element_end); + + /* + * count the byte length of a substring of 'name' having the same + * character count as the element + */ + substr_len = 0; + for (substr_nchars = 0; + substr_nchars < element_nchars && substr_len < name_len; + substr_nchars++) + { + substr_len += pg_mblen_range(name + substr_len, name_end); + } + + if (casefold_str_cmp(name, substr_len, lower_element, lower_element_len, mylocale)) + { + *len = substr_len; + pfree(lower_name); + return a - array; + } + + /* + * As last resort, try the case conversion and comparison for + * every substring from the beginning of the original string until + * we find a match. + */ + substr_len = 0; + while (substr_len < name_len) + { + substr_len += pg_mblen_range(name + substr_len, name_end); + + if (casefold_str_cmp(name, substr_len, + lower_element, lower_element_len, + mylocale)) + { + *len = substr_len; + pfree(lower_name); + return a - array; + } + } } - pfree(lower_element); } pfree(lower_name); @@ -2462,7 +2573,7 @@ from_char_seq_search(int *dest, const char **src, const char *const *array, char **localized_array, Oid collid, FormatNode *node, Node *escontext) { - int len; + size_t len; if (localized_array == NULL) *dest = seq_search_ascii(*src, array, &len); diff --git a/src/test/regress/expected/collate.linux.utf8.out b/src/test/regress/expected/collate.linux.utf8.out index fbaab7cdf83..9dee1dced71 100644 --- a/src/test/regress/expected/collate.linux.utf8.out +++ b/src/test/regress/expected/collate.linux.utf8.out @@ -479,6 +479,24 @@ SELECT to_date('01 Şub 2010', 'DD TMMON YYYY'); SELECT to_date('1234567890ab 2010', 'TMMONTH YYYY'); -- fail ERROR: invalid value "1234567890ab" for "MONTH" DETAIL: The given value did not match any of the allowed values for this field. +SELECT to_date('01 Aralık 2010', 'DD TMMONTH YYYY'); + to_date +------------ + 12-01-2010 +(1 row) + +SELECT to_date('01 aralık 2010', 'DD TMMONTH YYYY'); + to_date +------------ + 12-01-2010 +(1 row) + +SELECT to_date('2010 01 araLık', 'YYYY DD TMMONTH'); + to_date +------------ + 12-01-2010 +(1 row) + -- backwards parsing CREATE VIEW collview1 AS SELECT * FROM collate_test1 WHERE b COLLATE "C" >= 'bbc'; CREATE VIEW collview2 AS SELECT a, b FROM collate_test1 ORDER BY b COLLATE "C"; diff --git a/src/test/regress/sql/collate.linux.utf8.sql b/src/test/regress/sql/collate.linux.utf8.sql index 132d13af0a8..6d726ee9c99 100644 --- a/src/test/regress/sql/collate.linux.utf8.sql +++ b/src/test/regress/sql/collate.linux.utf8.sql @@ -188,6 +188,9 @@ SELECT to_date('01 ŞUB 2010', 'DD TMMON YYYY'); SELECT to_date('01 Şub 2010', 'DD TMMON YYYY'); SELECT to_date('1234567890ab 2010', 'TMMONTH YYYY'); -- fail +SELECT to_date('01 Aralık 2010', 'DD TMMONTH YYYY'); +SELECT to_date('01 aralık 2010', 'DD TMMONTH YYYY'); +SELECT to_date('2010 01 araLık', 'YYYY DD TMMONTH'); -- backwards parsing From ed8050370b7cf117dfb0de218695b1e003a51f36 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Wed, 5 Aug 2026 11:44:01 -0400 Subject: [PATCH 194/250] Make local buffers pin limit more conservative GetLocalPinLimit() and GetAdditionalLocalPinLimit(), currently in use only by the read stream, previously allowed a backend to pin all num_temp_buffers local buffers. This meant that the read stream could use every available local buffer for read-ahead, leaving none for other concurrent pin-holders. In 18, this was reported as a sequential scan of a temporary table failing with "no empty local buffer available": with effective_io_concurrency >= 64 and default io_combine_limit and temp_buffers, the scan's read stream budget, (effective_io_concurrency + 1) * io_combine_limit, meets or exceeds num_temp_buffers, so look-ahead could pin the entire local buffer pool. Any additional pin request made while returning tuples -- such as fetching TOASTed values from the table's (also temporary) TOAST table -- then found no unpinned buffer to evict. Cap the local pin limit to num_temp_buffers / 4, providing some headroom. This doesn't guarantee that all needed pins will be available -- for example, a backend can still open more cursors than there are buffers -- but it makes it less likely that read-ahead will exhaust the pool. This is a backpatch of commit da6874635db, which was applied to master only during the 19 development cycle, where the issue surfaced as a regression test failure after on-access pruning began setting the visibility map. Reports of the failure above on 18 prompted backpatching it now. No backpatch to 17, even though its read stream also allows a single stream to pin the whole pool in principle. There, sequential scans pass READ_STREAM_SEQUENTIAL, which disables fadvise-based look-ahead, so the look-ahead distance cannot grow past io_combine_limit (at most 32 buffers, versus a minimum temp_buffers of 100) and the reported failure is unreachable. The remaining theoretical paths require adversarial settings and have never been reported. Moreover, 17 predates these functions; the fix would have to be reimplemented in LimitAdditionalLocalPins(), making the change not a simple cherry-pick. Reported-by: Induja Sreekanthan Reported-by: Eduard Stepanov Reported-by: Feike Steenbergen Reported-by: Alexander Lakhin Reviewed-by: Xuneng Zhou Reviewed-by: Andres Freund Discussion: https://postgr.es/m/97529f5a-ec10-46b1-ab50-4653126c6889%40gmail.com Discussion: https://postgr.es/m/flat/CAFMO8-rYPSJbXsDdWDzDdpNi-fQ%2B6bKvgbXwE%2BR%3DsGko4epq0Q%40mail.gmail.com Backpatch-through: 18 --- src/backend/storage/buffer/localbuf.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c index ba26627f7b0..bda2bc97945 100644 --- a/src/backend/storage/buffer/localbuf.c +++ b/src/backend/storage/buffer/localbuf.c @@ -305,16 +305,24 @@ GetLocalVictimBuffer(void) uint32 GetLocalPinLimit(void) { - /* Every backend has its own temporary buffers, and can pin them all. */ - return num_temp_buffers; + /* + * Every backend has its own temporary buffers, but we leave headroom for + * concurrent pin-holders -- like multiple scans in the same query. + */ + return num_temp_buffers / 4; } /* see GetAdditionalPinLimit() */ uint32 GetAdditionalLocalPinLimit(void) { + uint32 total = GetLocalPinLimit(); + Assert(NLocalPinnedBuffers <= num_temp_buffers); - return num_temp_buffers - NLocalPinnedBuffers; + + if (NLocalPinnedBuffers >= total) + return 0; + return total - NLocalPinnedBuffers; } /* see LimitAdditionalPins() */ From b37f14875a284b7bf3c5ec2e3691aa5d8bf8e736 Mon Sep 17 00:00:00 2001 From: Amit Kapila Date: Thu, 6 Aug 2026 11:26:57 +0530 Subject: [PATCH 195/250] Fix race condition in subscription TAP test 023_twophase_stream. Buildfarm member olingo intermittently failed this test, timing out while waiting for the subscriber log to report an ERROR because max_prepared_transactions is zero there. The test captured the log offset only after issuing the publisher's BEGIN/INSERT/PREPARE TRANSACTION/COMMIT PREPARED sequence. Since streaming is enabled, the subscriber can receive and apply the transaction, and log the expected ERROR, before that publisher SQL command even returns, i.e. before the test captures the offset. The subsequent wait_for_log() calls then searched only from a point after the message had already been written, and timed out waiting for it. Fix by moving the offset capture to before the publisher's transaction is issued, ensuring it always precedes the point where the ERROR can appear in the subscriber log. Reported-by: Alexander Lakhin Author: Zhijie Hou Reviewed-by: Amit Kapila Backpatch-through: 16, where test was introduced Discussion: https://postgr.es/m/c43753d8-5265-4f77-83ff-9b1167276ec5@gmail.com --- src/test/subscription/t/023_twophase_stream.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/subscription/t/023_twophase_stream.pl b/src/test/subscription/t/023_twophase_stream.pl index dc629425daa..301c4d901c8 100644 --- a/src/test/subscription/t/023_twophase_stream.pl +++ b/src/test/subscription/t/023_twophase_stream.pl @@ -439,6 +439,8 @@ sub test_streaming )); $node_subscriber->restart; +$offset = -s $node_subscriber->logfile; + $node_publisher->safe_psql( 'postgres', q{ BEGIN; @@ -447,8 +449,6 @@ sub test_streaming COMMIT PREPARED 'xact'; }); -$offset = -s $node_subscriber->logfile; - # Confirm the ERROR is reported because max_prepared_transactions is zero $node_subscriber->wait_for_log( qr/ERROR: ( [A-Z0-9]+:)? prepared transactions are disabled/, From 585181e077486324e12f6071a70955a0a48906aa Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Thu, 6 Aug 2026 17:16:23 -0400 Subject: [PATCH 196/250] Restore vacuum failsafe abandonment of buffer access strategy VACUUM's wraparound failsafe mode exists to reclaim transaction IDs as quickly as possible. 4830f1024325 made the failsafe stop using the BAS_VACUUM buffer access strategy so that the rest of the vacuum could make use of all of shared buffers rather than being confined to the small strategy ring. However, when 9256822608f3 made vacuum's first heap pass use the read stream, this was accidentally disabled. The read stream keeps its own references to the buffer access strategy, so clearing vacrel->bstrategy in lazy_check_wraparound_failsafe() no longer had any effect on the reads issued by the first pass. Fix this by adding clearing the BufferAccessStrategy reference actually being used by the ongoing scan -- those in the ReadBuffersOperations structs themselves. Two things we accept rather than fix, as neither is worth the added complexity given how rarely failsafe mode is reached: - A small amount of read time for IOs that were already in progress when the strategy was cleared may be attributed to IOCONTEXT_NORMAL instead of IOCONTEXT_VACUUM. WaitReadBuffers() derives the IOContext from the (now cleared) strategy, so the wait time of these in-flight IOs is misattributed. This is bounded by the stream's look-ahead window and happens at most once per vacuum, when the strategy is first cleared. - The stream's buffer pin limit stays lower than it would have been had no strategy been used at all. max_pinned_buffers is capped by the strategy's pin limit when the stream is created and is not recomputed when the strategy is cleared. Raising it would mean building a new, larger ring, which would require first waiting for all in-progress IOs to complete. That didn't seem worth it. Reported-by: Jingtang Zhang Discussion: https://postgr.es/m/CAPsk3_APRYVLhAJ5TMwdmpSx8W_%3DPHMm%3DPmKAvnC3gBrfNommQ%40mail.gmail.com Backpatch-to: 18 --- src/backend/access/heap/vacuumlazy.c | 25 ++++++++++++++++++++++--- src/backend/storage/aio/read_stream.c | 22 ++++++++++++++++++++++ src/include/storage/read_stream.h | 1 + 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 8fbaf126756..7d22d0e7439 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -1307,6 +1307,15 @@ lazy_scan_heap(LVRelState *vacrel) PROGRESS_VACUUM_PHASE_SCAN_HEAP); } + /* + * If the wraparound failsafe has engaged -- either via the check + * above or during index vacuuming invoked from this loop -- stop + * using the buffer access strategy so that the rest of the vacuum may + * use all of shared buffers. + */ + if (unlikely(VacuumFailsafeActive)) + read_stream_clear_strategy(stream); + buf = read_stream_next_buffer(stream, &per_buffer_data); /* The relation is exhausted. */ @@ -2964,9 +2973,19 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel) VacuumFailsafeActive = true; /* - * Abandon use of a buffer access strategy to allow use of all of - * shared buffers. We assume the caller who allocated the memory for - * the BufferAccessStrategy will free it. + * We abandon use of the strategy in failsafe mode to allow use of all + * of shared buffers. vacrel->bstrategy is not the source of truth for + * an ongoing heap scan, but clear it just for tidiness. Any ongoing + * phase I heap scan has its own references to the strategy and clears + * them separately (see lazy_scan_heap()). And none of the other + * vacuum phases will read from vacrel->bstrategy once failsafe mode + * is engaged. The phase I read stream clears the strategy references + * held by the ReadBuffersOperations outside of this function because + * lazy_check_wraparound_failsafe() may be called from any phase of + * vacuum, including when the phase I stream is inactive. + * + * We assume the caller who allocated the memory for the + * BufferAccessStrategy will free it. */ vacrel->bstrategy = NULL; diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.c index 17e3a68a822..7f79514e962 100644 --- a/src/backend/storage/aio/read_stream.c +++ b/src/backend/storage/aio/read_stream.c @@ -1043,6 +1043,28 @@ read_stream_next_block(ReadStream *stream, BufferAccessStrategy *strategy) return read_stream_get_block(stream, NULL); } + +/* + * Stop using a buffer access strategy for reads from this stream. + * + * This clears the strategy for all of the stream's ReadBuffersOperations, + * including those with in-progress IOs. The completion of an IO whose + * strategy was cleared while it was in flight may have a small amount of its + * read time attributed to IOCONTEXT_NORMAL instead of the strategy's + * IOContext, because WaitReadBuffers() derives the IOContext from the (now + * cleared) strategy. This is bounded by the stream's look-ahead window and + * happens at most once, when the strategy is first cleared, so it is not worth + * the complexity of preserving the original IOContext for those IOs. + * + * Note that the caller is responsible for freeing the strategy's memory. + */ +void +read_stream_clear_strategy(ReadStream *stream) +{ + for (int i = 0; i < stream->max_ios; ++i) + stream->ios[i].op.strategy = NULL; +} + /* * Reset a read stream by releasing any queued up buffers, allowing the stream * to be used again for different blocks. This can be used to clear an diff --git a/src/include/storage/read_stream.h b/src/include/storage/read_stream.h index 9b0d65161d0..f27254a3eff 100644 --- a/src/include/storage/read_stream.h +++ b/src/include/storage/read_stream.h @@ -99,6 +99,7 @@ extern ReadStream *read_stream_begin_smgr_relation(int flags, ReadStreamBlockNumberCB callback, void *callback_private_data, size_t per_buffer_data_size); +extern void read_stream_clear_strategy(ReadStream *stream); extern void read_stream_reset(ReadStream *stream); extern void read_stream_end(ReadStream *stream); From 2fd8d45ecf7cee94613483f073c821b8834426c7 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 7 Aug 2026 14:23:35 +0900 Subject: [PATCH 197/250] Fix local pgstat entry leak on OOM during entry creation When pgstat_init_entry() fails due to an OOM in the DSA allocation, pgstat_get_entry_ref() cleaned up the shared hashtable but forgot to remove the local reference that pgstat_get_entry_ref_cached() had already inserted into pgStatEntryRefHash. Missing this cleanup would leave a backend with a stale local cache entry whose entry_ref points to a NULL shared_stats. If pgstat_gc_entry_refs() runs with this reference still around, it would crash due to a pointer dereference. The local reference is now removed before removing the shared entry, the order being sensitive to pending interrupts. Oversight in 8191e0c16a03. Author: Niall Newman Discussion: https://postgr.es/m/2FDAA194-9CF3-4FD7-A450-F1A4BEB125F6@turacolabs.com Backpatch-through: 15 --- src/backend/utils/activity/pgstat_shmem.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c index 056c24c6ee1..8889e4cac26 100644 --- a/src/backend/utils/activity/pgstat_shmem.c +++ b/src/backend/utils/activity/pgstat_shmem.c @@ -525,9 +525,12 @@ pgstat_get_entry_ref(PgStat_Kind kind, Oid dboid, uint64 objid, bool create, if (shheader == NULL) { /* - * Failed the allocation of a new entry, so clean up the - * shared hashtable before giving up. + * Failed the allocation of a new entry, so clean up both the + * local reference and the shared hashtable before giving up. + * Clean the local state first, since releasing the dshash + * lock can process a pending interrupt. */ + pgstat_release_entry_ref(key, entry_ref, false); dshash_delete_entry(pgStatLocal.shared_hash, shhashent); ereport(ERROR, From 4ea497a9263efe17d809b844a8cc0c71e99f5cc4 Mon Sep 17 00:00:00 2001 From: Etsuro Fujita Date: Fri, 7 Aug 2026 17:30:02 +0900 Subject: [PATCH 198/250] Drain pending asynchronous requests during ExecReScanAppend. The logic for asynchronous Append assumes that pending requests made for subplans of an Append are drained during ExecReScanAppend. To ensure that, commit 9e283fc85 modified postgresReScanForeignScan to drain such a request if any, but failed to take into account that if such a request was made for a subplan that is re-scanned with parameter changes or pruned in the next round by runtime pruning, the postgres_fdw callback function is called after ExecReScanAppend or never called, respectively. This would cause such a request to remain even after ExecReScanAppend, leading to incorrect results, an infinite loop, or an assertion failure. To fix, modify ExecReScanAppend to, for each of the pending requests, give the FDW a chance to drain that request using the existing ForeignAsyncConfigureWait/ForeignAsyncNotify callback functions. This makes the change made to postgresReScanForeignScan useless, so remove it as well. Back-patch to v14 where asynchronous Append was added. Reported-by: Alexander Korotkov Co-authored-by: Alexander Korotkov Co-authored-by: Gleb Kashkin Co-authored-by: Etsuro Fujita Reviewed-by: Alexander Pyhalov Reviewed-by: Gleb Kashkin Discussion: https://postgr.es/m/CAPpHfduMOTnV5Zj2KGJ7zanL_10QvccZHtPUaDfJvBhsh9axnQ%40mail.gmail.com Backpatch-through: 14 --- .../postgres_fdw/expected/postgres_fdw.out | 70 ++++++++++++- contrib/postgres_fdw/postgres_fdw.c | 15 +-- contrib/postgres_fdw/sql/postgres_fdw.sql | 14 ++- src/backend/executor/nodeAppend.c | 97 +++++++++++++++---- 4 files changed, 163 insertions(+), 33 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index a65f81d6eb6..e357c3170bc 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -11663,6 +11663,72 @@ SELECT * FROM result_tbl ORDER BY a; (3 rows) DELETE FROM result_tbl; +-- Test ExecAppendAsyncReset code path that drains outstanding async requests +-- (case where subplans are re-scanned with parameter changes) +EXPLAIN (VERBOSE, COSTS OFF) +SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE a = o.x OR a = 1505 LIMIT 1) s ORDER BY o.x; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------- + Sort + Output: "*VALUES*".column1 + Sort Key: "*VALUES*".column1 + -> Nested Loop + Output: "*VALUES*".column1 + -> Values Scan on "*VALUES*" + Output: "*VALUES*".column1 + -> Limit + Output: NULL::integer + -> Append + -> Async Foreign Scan on public.async_p1 async_pt_1 + Output: NULL::integer + Remote SQL: SELECT NULL FROM public.base_tbl1 WHERE (((a = $1::integer) OR (a = 1505))) + -> Async Foreign Scan on public.async_p2 async_pt_2 + Output: NULL::integer + Remote SQL: SELECT NULL FROM public.base_tbl2 WHERE (((a = $1::integer) OR (a = 1505))) + -> Async Foreign Scan on public.async_p3 async_pt_3 + Output: NULL::integer + Remote SQL: SELECT NULL FROM public.base_tbl3 WHERE (((a = $1::integer) OR (a = 1505))) +(19 rows) + +SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE a = o.x OR a = 1505 LIMIT 1) s ORDER BY o.x; + x +------ + 2505 + 3505 +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE a = o.x OR (a = 1505 AND o.x = 2505) LIMIT 1) s ORDER BY o.x; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------- + Sort + Output: "*VALUES*".column1 + Sort Key: "*VALUES*".column1 + -> Nested Loop + Output: "*VALUES*".column1 + -> Values Scan on "*VALUES*" + Output: "*VALUES*".column1 + -> Limit + Output: NULL::integer + -> Append + -> Async Foreign Scan on public.async_p1 async_pt_1 + Output: NULL::integer + Remote SQL: SELECT NULL FROM public.base_tbl1 WHERE (((a = $1::integer) OR ((a = 1505) AND ($1::integer = 2505)))) + -> Async Foreign Scan on public.async_p2 async_pt_2 + Output: NULL::integer + Remote SQL: SELECT NULL FROM public.base_tbl2 WHERE (((a = $1::integer) OR ((a = 1505) AND ($1::integer = 2505)))) + -> Async Foreign Scan on public.async_p3 async_pt_3 + Output: NULL::integer + Remote SQL: SELECT NULL FROM public.base_tbl3 WHERE (((a = $1::integer) OR ((a = 1505) AND ($1::integer = 2505)))) +(19 rows) + +SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE a = o.x OR (a = 1505 AND o.x = 2505) LIMIT 1) s ORDER BY o.x; + x +------ + 2505 + 3505 +(2 rows) + DROP FOREIGN TABLE async_p3; DROP TABLE base_tbl3; -- Check case where the partitioned table has local/remote partitions @@ -12443,8 +12509,8 @@ DROP TABLE base_tbl1; DROP TABLE base_tbl2; DROP TABLE result_tbl; DROP TABLE join_tbl; --- Test that an asynchronous fetch is processed before restarting the scan in --- ReScanForeignScan +-- Test ExecAppendAsyncReset code path that drains outstanding async requests +-- (case where subplans are re-scanned without parameter changes) CREATE TABLE base_tbl (a int, b int); INSERT INTO base_tbl VALUES (1, 11), (2, 22), (3, 33); CREATE FOREIGN TABLE foreign_tbl (b int) diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 4283ce9f962..ed23dedc542 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -1658,16 +1658,11 @@ postgresReScanForeignScan(ForeignScanState *node) return; /* - * If the node is async-capable, and an asynchronous fetch for it has - * begun, the asynchronous fetch might not have yet completed. Check if - * the node is async-capable, and an asynchronous fetch for it is still in - * progress; if so, complete the asynchronous fetch before restarting the - * scan. - */ - if (fsstate->async_capable && - fsstate->conn_state->pendingAreq && - fsstate->conn_state->pendingAreq->requestee == (PlanState *) node) - fetch_more_data(node); + * If the node is async-capable, any asynchronous fetch made for it should + * have been processed before we get here (see ExecAppendAsyncReset()). + */ + Assert(!fsstate->async_capable || !fsstate->conn_state->pendingAreq || + fsstate->conn_state->pendingAreq->requestee != (PlanState *) node); /* * If any internal parameters affecting this node have changed, we'd diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 2f7b3399198..c7802e4dd45 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -3991,6 +3991,16 @@ INSERT INTO result_tbl SELECT * FROM async_pt WHERE b === 505; SELECT * FROM result_tbl ORDER BY a; DELETE FROM result_tbl; +-- Test ExecAppendAsyncReset code path that drains outstanding async requests +-- (case where subplans are re-scanned with parameter changes) +EXPLAIN (VERBOSE, COSTS OFF) +SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE a = o.x OR a = 1505 LIMIT 1) s ORDER BY o.x; +SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE a = o.x OR a = 1505 LIMIT 1) s ORDER BY o.x; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE a = o.x OR (a = 1505 AND o.x = 2505) LIMIT 1) s ORDER BY o.x; +SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE a = o.x OR (a = 1505 AND o.x = 2505) LIMIT 1) s ORDER BY o.x; + DROP FOREIGN TABLE async_p3; DROP TABLE base_tbl3; @@ -4227,8 +4237,8 @@ DROP TABLE base_tbl2; DROP TABLE result_tbl; DROP TABLE join_tbl; --- Test that an asynchronous fetch is processed before restarting the scan in --- ReScanForeignScan +-- Test ExecAppendAsyncReset code path that drains outstanding async requests +-- (case where subplans are re-scanned without parameter changes) CREATE TABLE base_tbl (a int, b int); INSERT INTO base_tbl VALUES (1, 11), (2, 22), (3, 33); CREATE FOREIGN TABLE foreign_tbl (b int) diff --git a/src/backend/executor/nodeAppend.c b/src/backend/executor/nodeAppend.c index a11b36c7176..590d34e9e26 100644 --- a/src/backend/executor/nodeAppend.c +++ b/src/backend/executor/nodeAppend.c @@ -92,6 +92,7 @@ static void ExecAppendAsyncBegin(AppendState *node); static bool ExecAppendAsyncGetNext(AppendState *node, TupleTableSlot **result); static bool ExecAppendAsyncRequest(AppendState *node, TupleTableSlot **result); static void ExecAppendAsyncEventWait(AppendState *node); +static void ExecAppendAsyncReset(AppendState *node); static void classify_matching_subplans(AppendState *node); /* ---------------------------------------------------------------- @@ -423,6 +424,10 @@ ExecReScanAppend(AppendState *node) int nasyncplans = node->as_nasyncplans; int i; + /* If there are any async subplans, reset async requests made for them. */ + if (nasyncplans > 0) + ExecAppendAsyncReset(node); + /* * If any PARAM_EXEC Params used in pruning expressions have changed, then * we'd better unset the valid subplans so that they are reselected for @@ -458,25 +463,6 @@ ExecReScanAppend(AppendState *node) ExecReScan(subnode); } - /* Reset async state */ - if (nasyncplans > 0) - { - i = -1; - while ((i = bms_next_member(node->as_asyncplans, i)) >= 0) - { - AsyncRequest *areq = node->as_asyncrequests[i]; - - areq->callback_pending = false; - areq->request_complete = false; - areq->result = NULL; - } - - node->as_nasyncresults = 0; - node->as_nasyncremain = 0; - bms_free(node->as_needrequest); - node->as_needrequest = NULL; - } - /* Let choose_next_subplan_* function handle setting the first subplan */ node->as_whichplan = INVALID_SUBPLAN_INDEX; node->as_syncdone = false; @@ -1132,6 +1118,79 @@ ExecAppendAsyncEventWait(AppendState *node) } } +/* ---------------------------------------------------------------- + * ExecAppendAsyncReset + * + * Reset asynchronous requests made for async-capable subplans. + * ---------------------------------------------------------------- + */ +static void +ExecAppendAsyncReset(AppendState *node) +{ + int i; + + /* We should never be called when there are no async subplans. */ + Assert(node->as_nasyncplans > 0); + + /* + * Drain pending async requests if any. We force the as_syncdone flag to + * be true so that ExecAppendAsyncEventWait() waits until at least one + * event occurs. + */ + node->as_syncdone = true; + for (;;) + { + bool found = false; + + /* + * When called from ExecAppendAsyncEventWait(), postgres_fdw (and + * possibly other FDWs) will skip configuration of events for pending + * requests in some cases if as_needrequest isn't empty. To avoid + * that, discard results we already have. Note that we need to do + * this on every iteration, as the call to that function may produce + * new results. + */ + node->as_nasyncresults = 0; + bms_free(node->as_needrequest); + node->as_needrequest = NULL; + + i = -1; + while ((i = bms_next_member(node->as_asyncplans, i)) >= 0) + { + AsyncRequest *areq = node->as_asyncrequests[i]; + + if (areq->callback_pending) + { + found = true; + break; + } + } + if (!found) + break; + + CHECK_FOR_INTERRUPTS(); + + /* Wait or poll for async events. */ + ExecAppendAsyncEventWait(node); + } + + /* Reset async requests. */ + i = -1; + while ((i = bms_next_member(node->as_asyncplans, i)) >= 0) + { + AsyncRequest *areq = node->as_asyncrequests[i]; + + Assert(!areq->callback_pending); + areq->request_complete = false; + areq->result = NULL; + } + + /* Reset state variables. */ + Assert(node->as_nasyncresults == 0); + Assert(node->as_needrequest == NULL); + node->as_nasyncremain = 0; +} + /* ---------------------------------------------------------------- * ExecAsyncAppendResponse * From 7c25cdb1ebf64b1e1c9053ca27f9552c9a528a12 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Fri, 7 Aug 2026 09:59:55 -0400 Subject: [PATCH 199/250] Only clear VACUUM's read stream strategy once in failsafe mode 112c2683807b4d690 restored failsafe vacuum's abandonment of a buffer access strategy by clearing the ReadBuffersOperations' strategy references. But it did so in lazy_scan_heap()'s main loop, meaning it looped through all the ReadBuffersOperations once per block after failsafe was engaged. Track it with a local flag and clear the strategy only once. Reported-by: Melanie Plageman Discussion: https://postgr.es/m/CAAKRu_Zse14nSNeCgtnE1LUAH8Of7OmYR%2BCc3O_DAzxt3m6T-g%40mail.gmail.com Backpatch-through: 18 --- src/backend/access/heap/vacuumlazy.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 7d22d0e7439..989fb491e55 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -1206,6 +1206,7 @@ lazy_scan_heap(LVRelState *vacrel) BlockNumber orig_eager_scan_success_limit = vacrel->eager_scan_remaining_successes; /* for logging */ Buffer vmbuffer = InvalidBuffer; + bool strategy_cleared = false; const int initprog_index[] = { PROGRESS_VACUUM_PHASE, PROGRESS_VACUUM_TOTAL_HEAP_BLKS, @@ -1311,10 +1312,14 @@ lazy_scan_heap(LVRelState *vacrel) * If the wraparound failsafe has engaged -- either via the check * above or during index vacuuming invoked from this loop -- stop * using the buffer access strategy so that the rest of the vacuum may - * use all of shared buffers. + * use all of shared buffers. Failsafe mode stays engaged once + * triggered, so we only need to do this once. */ - if (unlikely(VacuumFailsafeActive)) + if (unlikely(VacuumFailsafeActive) && !strategy_cleared) + { read_stream_clear_strategy(stream); + strategy_cleared = true; + } buf = read_stream_next_buffer(stream, &per_buffer_data); From 311e66df9cc857dcbb02024270f5130815682d63 Mon Sep 17 00:00:00 2001 From: Fujii Masao Date: Sat, 8 Aug 2026 00:09:05 +0900 Subject: [PATCH 200/250] Fix hot standby accepting connections too early after a crash reset Commit b53b88109f9 made the postmaster maintain reachedConsistency in addition to the startup process. Since the startup process is forked from the postmaster, it begins life holding whatever value the postmaster last set. On a crash reset the postmaster re-forks the startup process while its own copy still says true: it clears that copy only on receipt of PMSIGNAL_RECOVERY_STARTED, which the replacement process cannot send before it exists. The replacement therefore starts out believing the database is already consistent. CheckRecoveryConsistency() then skips the minRecoveryPoint comparison altogether, so hot standby is announced at redo start while replay may be arbitrarily far behind minRecoveryPoint. Read-only connections are accepted and answer from heap pages that were flushed ahead of the replay position, returning wrong results with no error raised. The same branch also runs XLogCheckInvalidPages() and CheckTablespaceDirectory(), which are skipped as well, and log_invalid_page() treats page references that are normal before consistency as a PANIC. Fix by clearing reachedConsistency in InitWalRecovery(), so that a startup process never depends on the value it inherited. The postmaster's own copy is deliberately left alone: forked backends read it to choose the "not yet accepting connections" errdetail, and it converges once the new startup process sends PMSIGNAL_RECOVERY_STARTED and, on reaching minRecoveryPoint, PMSIGNAL_RECOVERY_CONSISTENT. Successive crash resets alternate. A startup process that skips the branch never sends PMSIGNAL_RECOVERY_CONSISTENT, so the postmaster's copy stays false and the next reset forks a process holding the correct value; that pass reaches consistency properly, which sets the postmaster's copy back to true and re-arms the problem for the reset after it. Roughly every other crash reset is therefore affected, not just the first one. EXEC_BACKEND builds are unaffected, as reachedConsistency is not carried in BackendParameters. Backpatch to v18, where commit b53b88109f9 introduced this issue. Reported-by: Eric Ridge Author: Nikhil Sontakke Reviewed-by: Fujii Masao Discussion: https://postgr.es/m/CA+UBoq2n2Zg9rKgMfUtUohzGssisF9cDeyjKqPrnRNFprEyX1Q@mail.gmail.com Backpatch-through: 18 --- src/backend/access/transam/xlogrecovery.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index 476187113d4..37d6090e465 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -531,6 +531,15 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, dbstate_at_startup = ControlFile->state; + /* + * A startup process always starts with an inconsistent database. Set the + * flag accordingly, even if it was inherited from a postmaster that had + * already marked the database as consistent. This keeps the invariant + * local to the startup process without requiring every fork path to clear + * the flag. + */ + reachedConsistency = false; + /* * Initialize on the assumption we want to recover to the latest timeline * that's active according to pg_control. From 828d602f21156de916397b5df52397f18da7f759 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Fri, 7 Aug 2026 13:11:52 -0400 Subject: [PATCH 201/250] First-draft release notes for 18.5. As usual, the release notes for other branches will be made by cutting these down, but put them up for community review first. --- doc/src/sgml/release-18.sgml | 2646 ++++++++++++++++++++++++++++++++++ 1 file changed, 2646 insertions(+) diff --git a/doc/src/sgml/release-18.sgml b/doc/src/sgml/release-18.sgml index 222d884831b..e2e3cf237f2 100644 --- a/doc/src/sgml/release-18.sgml +++ b/doc/src/sgml/release-18.sgml @@ -1,6 +1,2652 @@ + + Release 18.5 + + + Release date: + 2026-08-13 + + + + This release contains a variety of fixes from 18.4. + For information about new features in major release 18, see + . + + + + Migration to Version 18.5 + + + A dump/restore is not required for those running 18.X. + + + + However, if you have any GIN indexes, see the changelog entry below + about possibly-corrupt reltuples values for + their tables. + + + + Also, if you use contrib/btree_gist + or contrib/ltree, you may need to reindex indexes + made with those extensions; see the relevant entries below. + + + + Also, if you are upgrading from a version earlier than 18.2, + see . + + + + + Changes + + + + + + + Ensure that parallel GIN index builds update the table's + pg_class.reltuples + value correctly (Jan Nidzwetzki, Tomas Vondra) + § + + + + A parallel worker could report an uninitialized value for the + number of rows it processed, leading to a bogus value + for reltuples, even Infinity or NaN. + Such values could lead to subsequent autovacuum and autoanalyze + operations never deciding that the table needs to be processed. + If so, the situation will not self-heal. A manual + ANALYZE command, or creation of another index, + will be needed to reset reltuples to + the correct value. If you have any tables with GIN indexes, it's + recommended to check to see if + their reltuples entries look sane. + A query such as this may be helpful: + +SELECT DISTINCT t.oid::regclass, t.reltuples +FROM pg_class t + JOIN pg_index i ON t.oid = i.indrelid + JOIN pg_class ic ON i.indexrelid = ic.oid +WHERE t.relhasindex AND ic.relam = 2742; + + + + + + + + Fix self-deadlock when replaying WAL generated by an older minor + version (Andrey Borodin) + + + + This error was introduced in the previous set of minor releases. It + caused standby servers that were following a primary of an older + minor release version to get stuck in some scenarios. + + + + + + + Fix mis-handling of asynchronous reads when rescanning an + asynchronous Append plan node (Alexander Korotkov, Gleb Kashkin, + Etsuro Fujita) + § + + + + When an upper plan node rescans an Append before having read the + entire Append output, we need to discard any in-flight requests sent + to external servers (by postgres_fdw for + example). This was not done correctly in cases where a subplan has + parameter changes or is discarded by partition pruning in the next + scan. The outcome could be incorrect query results, an infinite + loop, or an assertion failure. + + + + + + + Fix error in partition pruning for RANGE-partitioned tables + (David Rowley) + § + + + + In some cases the DEFAULT partition would be skipped when it should + not be, which could lead to rows missing from query results. + + + + + + + Correctly update foreign-data-wrapper state in a ModifyTable plan + node after pruning result relations (Ayush Tiwari, Rafia Sabih) + § + § + + + + Previously, if run-time partition pruning determined that some + partitions of a partitioned target table need not be scanned + and the table had any foreign-table partitions, a crash + or erroneous behavior was likely. + + + + + + + Fix missed concurrent update in UPDATE + with RETURNING OLD on a table that has + a BEFORE UPDATE trigger (Dean Rasheed) + § + + + + If the target row was concurrently updated, then at isolation + level READ COMMITTED any OLD + values in RETURNING should reflect the updated + row. But stale values were returned if there was a trigger + (although the trigger itself, and the final output row, saw the + correct values). + + + + + + + Fix hash join performance issue when there are multiple join keys + and many NULL values (David Rowley) + § + + + + Null-keyed tuples should not get inserted into the hash table, since + they will never match any other tuples. The code got this wrong if + the null was in a non-last join column, bloating the hash table + quite a lot if many inputs contain nulls. + + + + + + + Fix parsing of + parenthesized OLD/NEW + in RETURNING expressions (Marko Grujic) + § + + + + Expressions such as (old).colname + and (old).* were mis-handled, effectively + converting them to NEW references. + + + + + + + Fix planner's nullability and strictness checks + for value IN + (array) expressions + (Ayush Tiwari) + § + + + + These checks should only succeed if the array operand is known to be + non-empty, but that consideration was missed, allowing optimizations + to be applied that should not be. This could result in wrong query + answers if the array actually was empty. + + + + + + + Fix incorrect join removal logic (Matheus Alcantara, Richard Guo) + § + § + + + + In edge cases, it was possible for a constant output value coming + from within the nullable side of an outer join to not be replaced by + NULL when it should be. + + + + + + + Clean up PlaceHolderVars more thoroughly during join removal + (Richard Guo, Arne Roland) + § + § + + + + This fix corrects various edge cases that could trip assertions or + result in incorrect plans. + + + + + + + Add missed checks for hashability of equality comparisons on + container datatypes (arrays, composites, ranges) (Andrei Lepikhov, + Tom Lane) + § + + + + The planner must verify hashability of the container's component + type(s) before deciding it can use a hash-based plan type. This + step was missed in some places, leading to could not identify + a hash function failures at execution. + + + + + + + Avoid pushing WHERE clauses down past a grouping + step that has a different equivalence rule (Richard Guo) + § + + + + A test on a grouping column that is grouped by a nondeterministic + collation is safe to push down only if it is a comparison using that + same collation. Otherwise it might filter some rows the grouping + would have merged. + + + + + + + Fix mis-optimization of COUNT window functions + that have an EXCLUDE clause or + lack ORDER BY (Chengpeng Yan, David Rowley) + § + + + + These window functions were treated as monotonic when they should + not be, allowing wrong answers to be computed. + + + + + + + Avoid cache lookup failed for collation 0 error when + planner looks up statistics for a column of type "char" + (Feng Wu) + § + + + + + + + Fix ALTER COLUMN ... DROP EXPRESSION to work when + there are multiple levels of partitions (Alberto Piai) + § + + + + + + + Fix attaching partitions of indexes that are exclusion constraints + (Japin Li) + § + + + + Notably, this oversight broke dump/restore of partitioned exclusion + constraints. + + + + + + + Prevent setting NO INHERIT on + partitioned NOT NULL constraints + via ALTER CONSTRAINT (Andreas Karlsson) + § + + + + NOT NULL constraints on partitioned tables are + supposed to be inherited by all partitions, and therefore must not + be marked NO INHERIT. This rule was correctly + enforced by constraint creation, but not by ALTER TABLE + ... ALTER CONSTRAINT. + + + + + + + Disallow renaming a rule to _RETURN (Tom Lane) + § + + + + That name is reserved for a view's ON SELECT + rule, but ALTER RULE allowed renaming other rules + to _RETURN, causing trouble later. + + + + + + + Fix missing lock release for role membership grants in DROP + OWNED BY (Jeff Davis) + § + + + + This oversight resulted in a warning message, followed by retaining + a lock on the membership grant until the end of the transaction. + + + + + + + Fix failure of EXPLAIN when + deparsing SQL/JSON aggregates (Richard Guo) + § + + + + Some plan structures resulted in invalid + JsonConstructorExpr underlying node type errors. + + + + + + + Fix use of REINDEX CONCURRENTLY with + a deferred uniqueness constraint (Nitin Motiani) + § + + + + The transient index copy created during REINDEX + CONCURRENTLY was incorrectly marked as enforcing immediate + uniqueness, causing spurious reports of constraint violation. + + + + + + + Fix LIKE matching with nondeterministic + collations and backslashes (Nitin Motiani, Tom Lane) + § + § + + + + When using a nondeterministic collation, LIKE + mishandled an escaped backslash (\\), treating it + as effectively not there. It also did the wrong thing with a + leading backslash preceding an ordinary character; in that case the + backslash should be effectively ignored, but it caused the ordinary + character to be matched exactly rather than allowing the + nondeterministic collation to decide if there's a match. + + + + + + + Fix LIKE/regex optimization for indexscan with + exact-match pattern (Jelte Fennema-Nio) + § + + + + Refactoring for LIKE with non-deterministic + collations accidentally broke the optimization for converting + a LIKE or regex exact-match pattern to an + equality index condition when the index collation doesn't match + the expression collation. Among other things, that + made psql's + \d tablename + command much slower. + + + + + + + Fix matching of localized month/day names + in to_date() (Heikki Linnakangas) + § + + + + The matching logic misbehaved in cases where case-folding changes + the byte length of the string. + + + + + + + Correct case-folding rules for Greek final sigma (Jeff Davis) + § + + + + If the string is preceded only by Case Ignorable characters, don't + consider it to be a final sigma. This only affects the + built-in pg_unicode_fast locale. + + + + + + + Fix incorrect NFC recomposition for Hangul U+11A7 (TBASE) + (Diego Frias, Michael Paquier) + § + + + + This character was treated as a valid T syllable, which it is not, + and hence silently swallowed during normalization. + + + + + + + Avoid possible truncation of output lexemes in case-insensitive + synonym dictionaries (Jeff Davis) + § + + + + If folding to lower case increased the byte length of a lexeme, it + was incorrectly truncated to its original byte length when emitted. + + + + + + + Defend against truncated UTF-8 characters in case-conversion logic + (Jeff Davis) + § + + + + + + + Fix typo in hash_record_extended() (Man Zeng) + § + + + + The code failed to initialize the second isnull argument passed to + FunctionCallInvoke(). This is harmless for existing in-core + extended hash support functions, which will not examine that value. + However, extension-provided hash functions could be affected if they + inspect PG_ARGISNULL(1). + + + + + + + Fix pg_get_publication_tables() to not fail if + a publishable table is dropped concurrently (Bharath Rupireddy) + § + + + + + + + Prevent satisfies_hash_partition() from + crashing with VARIADIC NULL (Robert Haas) + § + + + + + + + Report invalid-weight errors more cleanly and consistently + in tsvector_filter() and allied functions + (Ewan Young) + § + + + + In particular, report weight characters that are not printable ASCII + in octal form (\nnn), + as charout() would render them. This avoids + possibly producing an invalidly-encoded error message. + + + + + + + Reject out-of-range timestamp shift values + in uuidv7() (Baji Shaik) + § + + + + The shift value must not be so large as to produce a timestamp out + of the range that a v7 UUID can represent. Previously, a garbage + UUID value was produced. + + + + + + + Fix mishandling of namespace nodes in xpath() + (Michael Paquier) + § + § + + + + This fix avoids an unexpected could not copy node + error. + + + + + + + Fix jsonpath's .decimal method to + not throw a hard error for incorrect precision or scale (Ewan Young) + § + § + + + + Silent mode should suppress these errors, but failed to. + + + + + + + Treat an undefined jsonpath variable as an error even when no + variables are supplied (Andrey Rachitskiy) + + + + The jsonb @? + and @@ operators cannot supply any variables to + be used in their jsonpath expressions. This code path erroneously + treated an unknown jsonpath variable as a JSON null, rather than + raising an error as expected. Aside from not being the expected + behavior, this mistake could result in unbounded memory consumption. + + + + + + + Fix NULL-pointer crash when IS JSON or similar + constructs have an argument that is of string category but lacks a + cast to type text (Ayush Tiwari) + § + + + + There are no such data types in + core PostgreSQL, but the problem is + reachable with some extension types. + + + + + + + Ensure that SQL/JSON ON EMPTY / ON ERROR + DEFAULT values are coerced to the correct typmod (Ewan Young) + § + + + + For example, the declared precision and scale of + a numeric target column were not applied to the default + value. + + + + + + + Avoid machine-dependent behavior when dividing the smallest + possible money value by -1 (Andrey Rachitskiy) + § + + + + + + + Fix crash after out-of-memory failure partway through creation of a + cache entry for a text search dictionary (Tom Lane) + § + + + + + + + Fix memory-safety bugs in processing of incorrect ispell/hunspell + dictionary files (Andrey Rachitskiy) + § + + + + + + + Prevent access to other sessions' temporary tables (Jim Jones, + Daniil Davydov, Alexander Korotkov) + § + § + + + + Some code paths failed to prevent this, leading to silently wrong + (inconsistent) results. + + + + + + + Prevent no empty local buffer available errors during + temporary table access (Melanie Plageman) + § + + + + Limit the number of local buffers that the read streaming mechanism + is allowed to use. Previously, a large value + of effective_io_concurrency could allow a single + stream to use all the buffers, resulting in failure. + + + + + + + Fix the order in which autovacuum processes databases + (Rustam Khamidullin) + § + + + + It was unintentionally processing databases from lowest to highest + score, when it should be doing the reverse. + + + + + + + Restore full use of shared buffer pool + in VACUUM's wraparound failsafe mode + (Melanie Plageman) + § + § + + + + An ordinary VACUUM is limited to use just a few + shared buffers, so as not to impinge too much on other processing. + However, in failsafe mode we want to reclaim transaction IDs as + quickly as possible, so that limit is supposed to be abandoned + to allow vacuuming to proceed as fast as possible. This behavior + was accidentally broken during refactoring in v18; restore it. + + + + + + + Fix memory leak in parallel vacuum worker processes (Baji Shaik) + § + + + + Progress reports from a parallel worker leaked about 1kB per report, + with the waste accumulating for the life of the worker process. + + + + + + + Honor query cancel and vacuum delay during GIN index posting-tree + cleanup (Paul Kim, Alexander Korotkov) + § + + + + The posting tree for a common value can be large, so that this + missed check could allow vacuum to run for a long time before + noticing an interrupt. + + + + + + + Fix possible mis-decoding of index tuples during GiST and SP-GiST + index-only scans (Peter Geoghegan) + § + + + + This error could lead to emitting corrupted data from an index-only + scan plan. The only affected core opclass is GiST's range_ops, and + it could only fail if the range column were not the first index + column. + + + + + + + Ensure that the new last block of a bulk-extended table is added to + its free space map promptly (Jingtang Zhang) + § + + + + An off-by-one error caused the last block of a multi-block table + extension to not be marked as free in the map. This would + eventually get corrected by vacuum, but meanwhile the space wouldn't + be used. + + + + + + + Avoid possible double-free or infinite error recovery loop in + resource cleanup during transaction abort (Tom Lane) + § + + + + + + + When creating directories, tolerate concurrent creation of the same + directory (Andrew Dunstan, Tom Lane) + § + + + + + + + Fix JIT-compiled tuple deconstruction code to account correctly for + virtual generated columns (David Rowley) + § + + + + + + + Prevent creation of dangling object dependencies by acquiring a + shared lock on any object being depended on (Bertrand Drouvot) + § + § + + + + The shared lock will conflict with any attempt to drop the + depended-on object, eliminating the race condition that formerly + existed. For example, if one session drops a schema (that appears + empty to it) concurrently with some other session creating a + function in that schema, previously both transactions could commit, + leaving an invalid function definition behind. Now, one transaction + or the other will fail. + + + + + + + Fix race condition in conflict detection + for SERIALIZABLE isolation mode + (Peter Geoghegan) + § + + + + A conflict could be missed when examining an initially-empty btree + index, allowing failure of serializability due to improperly + allowing conflicting transactions to commit. + + + + + + + Fix race condition in ProcSignalBarrier code (Masahiko Sawada) + § + + + + This error could result in processes getting stuck, typically after + reporting still waiting for backend with + PID nnnn to accept + ProcSignalBarrier. + + + + + + + Fix race conditions when a set of processes that belong to the same + lock group exit at the same time (Vlad Lesin) + § + § + + + + These errors could lead to PANIC aborts, with messages such + as latch already owned. The issue does not normally + arise in regular parallel query, since the leader won't exit before + seeing its workers finish; but some extensions reach the problem. + + + + + + + Fix WAL logging of operations that clear bits in tables' visibility + maps (Melanie Plageman, Andres Freund) + § + § + § + + + + Such VM changes were missed by the WAL summarizer, potentially + leading to incorrect incremental backups. We also failed to log + full-page images of such VM pages when needed, potentially allowing + torn page writes to go uncorrected. This could lead to misbehavior + later, such as wrong results from index-only scans. + + + + + + + Prevent WAL summarizer process from getting stuck at a timeline + switch (Robert Haas) + § + § + + + + + + + Fix race with timeline selection in logical decoding during standby + promotion (Bertrand Drouvot) + § + § + + + + Logical decoding being performed on the standby could fail with + a requested WAL segment has already been removed + error. A repeat attempt would succeed, so there was no permanent + problem but there was an availability hazard. + + + + + + + Avoid exposing a WAL receiver's full connection string during timeline + jumps (Chao Li) + § + + + + The pg_stat_wal_receiver view should show a + sanitized version of the connection string, without sensitive data. + But it transiently showed the full string when we re-use an existing + WAL receiver. + + + + + + + Use run-time checks, not just Asserts, to verify the correct number + of columns in tuples received during logical replication (Varik + Matevosyan) + § + + + + A malicious or buggy publisher could send inconsistent numbers of + columns. While we could not find a scenario in which this would + have serious ill effects, extra caution seems warranted. + + + + + + + Clean up quoting of string parameters within constructed replication + commands (Tom Lane) + § + + + + Various places that generate replication commands were not being + adequately careful about quoting replication slot names and other + parameters that need to be inserted into those commands. This could + result in unexpected syntax errors in those commands. In principle, + a crafted replication slot name could result in SQL injection; but + such a scenario seems very unlikely to occur in practice, since + replication operations can only be invoked by highly-privileged + users and there is no reason for them to use a slot name coming from + an untrustworthy source. + + + + + + + Fix logical decoding of empty prepared transactions (Masahiko Sawada) + § + + + + A prepared transaction that did not cause any decodable updates could + result in sending COMMIT/ROLLBACK PREPARED to the + output plugin with no preceding PREPARE. For the + built-in subscriber this breaks replication, and other plugins will + probably not like it either. + + + + + + + Fix corruption of unlogged sequences after standby promotion + (Fujii Masao) + § + + + + Previously, if an unlogged sequence was created on the primary and + replicated to a standby, accessing the sequence after promoting the + standby could fail with bad magic number in sequence + or related errors. + + + + + + + Fix cascading standby reconnect failure after archive fallback + (Marco Nenciarini) + § + + + + A cascading standby could fail to reconnect to its upstream standby + with requested starting point ... is ahead of the WAL flush + position after falling back to archive recovery. + + + + + + + Prevent accepting hot-standby connections before WAL replay has + reached a consistent database state (Nikhil Sontakke) + § + + + + + + + Do not try to clear + pg_database.dathasloginevt + locally on a standby server (Ayush Tiwari) + § + + + + Event trigger cleanup tried to perform that action on standby + servers as well as the primary. That can't work on a standby, + and there's no need anyway since replay of the primary's database + change will soon fix it. + + + + + + + Avoid race condition while dropping obsolete replication slots + (Xuneng Zhou) + § + + + + An incorrect unlock and log message could occur if another session + immediately re-used the dropped slot's shared-memory entry. + + + + + + + Avoid race condition while dropping ephemeral replication slots + (Zhijie Hou) + § + + + + The slot-releasing code performed some additional updates to the + replication slot's shared-memory entry after releasing the slot. + This is unsafe since another session could immediately re-use the + dropped slot's shared-memory entry. Skip those updates in the case + of an ephemeral slot. + + + + + + + Fix stale progress reports during logical replication table + synchronization (Shinya Kato) + § + + + + Previously, the pg_stat_progress_copy view + in the subscriber would continue to show the + initial COPY operation as active even after the + data copy had finished. The stale entry remained visible until + synchronization caught up with the publisher. + + + + + + + Clear base backup progress on backup failure (Chao Li) + § + § + + + + Previously the pg_stat_progress_basebackup + view would continue to show a stale progress entry after a failure, + until the replication client + disconnected. pg_basebackup normally + disconnects immediately, but other clients might not. + + + + + + + Fix possible PANIC due to concurrent drop of pgstats entries + when track_functions is enabled (Sami Imseih, + Michael Paquier) + § + § + § + + + + + + + Clean up broken local pgstats entry after failing to obtain space for + the corresponding shared hashtable entry (Niall Newman) + § + + + + Failure to do this led to a null-pointer dereference the next time + the local entry was used. + + + + + + + Avoid recording incorrect I/O operation statistics after a failed + read or write (Bertrand Drouvot) + § + + + + + + + In PL/Perl, avoid NULL pointer + dereference crash when working with an + invalid PostgreSQL::InServer::ARRAY object (Xing Guo) + § + + + + + + + In PL/Python, properly check for errors + when working with sequence and mapping objects (Richard Guo) + § + + + + Previously, a broken object or an unhandled exception could result + in a NULL pointer dereference crash. + + + + + + + In libpq, always drain all pending bytes + from the SSL or GSS decryption buffer + during pqReadData() (Jacob Champion) + § + § + § + § + + + + This avoids edge cases where libpq or its + calling application waits for more data to arrive on the socket, but + actually all the data has already arrived. + + + + + + + Improve libpq's handling of out-of-memory + conditions (Anthonin Bonnefoy) + § + + + + + + + Fix libpq's trace facility to print + new-style BackendKeyData and CancelRequest messages correctly + (Anthonin Bonnefoy) + § + + + + + + + Allow libpq to accept + ParameterDescription messages exceeding 30000 bytes (Ning Sun) + § + + + + Previously, this message type was not among those + that libpq's validity heuristics believed + could be long. The limit resulted in failure for prepared queries + having more than 7498 parameters, which is unlikely but supported. + + + + + + + Fix null-pointer crash in ecpg compiler + (Jehan-Guillaume de Rorthais) + § + + + + ecpg failed on + a DECLARE section containing a union nested + inside a struct. + + + + + + + Reject multiple descriptor header items + in ecpg's GET/SET + DESCRIPTOR statements (Masashi Kamura) + § + + + + Previously the grammar allowed this syntax, but broken C code was + generated. Adjust the grammar and the documentation to allow only + one header item. + + + + + + + Fix issues with deferred errors in pipeline mode + in psql (Michael Paquier) + § + + + + psql could get stuck or suffer an + assertion failure in some scenarios where the server reports an + error in response to a Sync message, such as a deferred constraint + violation. + + + + + + + Make line widths match in psql's expanded + aligned output format (Pavel Stehule) + § + + + + When the table's data rows are narrower than the record header lines, + widen the data rows to match the headers, avoiding unsightly output. + + + + + + + Enforce the intended upper limit + for psql's special + variable WATCH_INTERVAL + (Sven Klemm, Daniel Gustafsson) + § + + + + If a too-large value was given, psql + reported an error but applied the setting anyway. + + + + + + + Fix psql's privilege check for showing + database size in \l+ (Christoph Berg) + § + + + + The underlying server function permits users who + have pg_read_all_stats privileges to see the + sizes of all databases, even if they lack CONNECT + privilege. But psql was unaware of that + provision and would not call the function unless the user + has CONNECT privilege. + + + + + + + Fix psql's tab completion + for \df to consider procedures too + (Erik Wienhold) + § + + + + + + + Fix thread-safety bug in pgbench + (Fujii Masao) + § + + + + When pgbench runs with multiple threads + and the option, different threads + could attempt to use the same buffer to construct error messages, + leading to corrupted log output. + + + + + + + In pg_combinebackup, prevent infinite + loop if the source file is shorter than expected (Peter Eisentraut) + § + + + + + + + Fix cleanup of publisher-side objects after errors + in pg_createsubscriber (Nisha Moond) + § + + + + When pg_createsubscriber fails after + creating logical replication objects, it should remove the + publication and replication slot that it created on the publisher. + Some error cases failed to do so. + + + + + + + Use the source cluster's group-read file permissions + for pg_recvlogical output files + (Fujii Masao) + § + + + + pg_recvlogical was documented to behave + this way, but it never actually enabled group-read. + + + + + + + Fix inconsistent behavior of pg_restore + with + or + (Chao Li, Michael Paquier) + § + § + + + + When combined with other selective-restore options such + as , these options failed to restore the + expected items, unlike pg_dump with + similar options. + + + + + + + Fix vacuumdb --missing-stats-only to ignore + partitioned expression indexes (Baji Shaik) + § + + + + Previously, vacuumdb would always attempt + to ANALYZE the partitioned table, accomplishing + nothing since statistics are never created for partitioned indexes, + only for their leaf indexes. + + + + + + + In contrib/amcheck, fix failure to report + corruption of a btree metapage's allequalimage + flag (Chao Li) + § + + + + + + + In contrib/amcheck, fix query-lifespan memory + leak while verifying a GIN index (Kirill Reshke) + § + + + + + + + In contrib/amcheck, handle short-header varlena + datums correctly (Andrey Borodin) + § + + + + This error could result in doing excess work while verifying a btree + index, but seems not to have had any worse consequences. + + + + + + + In contrib/btree_gist, + fix NaN handling in the float4 + and float8 opclasses (Bill Kim, Tom Lane) + § + + + + Comparisons, as well as the GiST penalty and distance functions, did + not account for NaN and would give the wrong + answer when handed one. It is recommended to + reindex btree_gist indexes on float columns + after installing this update, if there is any possibility that there + are NaN entries in those columns. + + + + + + + In contrib/btree_gist, fix sorting + of bit/varbit entries during GiST index + construction (Tom Lane) + § + + + + Values of bit types were sorted as though they + were byteas, which did not cause any obvious failure + but would result in an inefficient index, since the types' + representations are different. It is recommended to + reindex btree_gist indexes on bit columns + after installing this update. + + + + + + + In contrib/btree_gist, fix searches using a + not-equal operator (Ayush Tiwari) + § + + + + For variable-length data types, the code for scanning non-leaf index + pages applied the wrong comparison function, leading to wrong + results and potentially crashes. + + + + + + + In contrib/dblink + and contrib/postgres_fdw, ensure that a user-mapping + setting for use_scram_passthrough overrides one + for a foreign server (Matheus Alcantara) + § + § + + + + Previously the precedence went the other way, but that is + inconsistent with the behavior of other foreign-table options. + + + + + + + Reject setting use_scram_passthrough + on contrib/dblink foreign-data wrappers + (Matheus Alcantara) + § + + + + This option is only meaningful on foreign servers and user + mappings, but dblink incorrectly allowed it at the FDW level as + well (and then ignored it). + + + + + + + Fix unguarded recursion and loops in + contrib/hstore_plperl, + contrib/jsonb_plperl, and + contrib/jsonb_plpython + (Aleksander Alekseev) + § + § + + + + Prevent stack overflow when dealing with deeply + nested jsonb values, and allow interruption of the infinite + loop caused when attempting to dereference circular chains of Perl + object references. + + + + + + + Fix missed release of statistics catcache entry + in contrib/intarray (Man Zeng) + § + + + + This oversight led to warnings like resource was not closed: + cache pg_statistic. + + + + + + + In contrib/ltree, fix integer overflow in + comparisons (Ayush Tiwari) + § + + + + ltree values containing more than about 14,653 labels + resulted in wrong comparison answers due to overflow. If a btree + index contains such values, it is probably corrupt and should be + reindexed after installing this update. + + + + + + + In contrib/pgcrypto, avoid double-free crash + after encountering an error while using an OSSLCipher object + (Yuelin Wang) + § + + + + + + + Fix out-of-bounds access + in contrib/pg_prewarm's autoprewarm worker + (Matheus Alcantara) + § + + + + The code tried to fetch a value from one past the end of an array, + risking a segfault. + + + + + + + Fix array overrun in contrib/pg_surgery's + heap_force_kill and + heap_force_freeze functions (Michael Paquier) + § + + + + Attempting to change a TID whose offset number equals + MaxHeapTuplesPerPage wrote one byte past the end of the allocated + array, potentially crashing the server. + + + + + + + In contrib/pg_surgery, avoid infinite loop with + TID arrays having more than 64K elements (Andrey Rachitskiy) + § + + + + + + + Avoid NULL-pointer dereference + in contrib/refint's + check_foreign_key() (Ayush Tiwari) + § + + + + In the on-update-cascade case, a null value of a referenced column + led to a crash. This is an oversight in the fix for CVE-2026-6637, + but the code that was there before that wasn't really right either. + + + + + + + Remove the plan cache in contrib/refint + (Ayush Tiwari) + § + + + + This caching behavior has several serious bugs, notably that + check_foreign_key() embeds the new key values + in its cascade-UPDATE queries, so a cached plan reuses the + originally-needed values rather than the key values that should be + used. The simplest solution is to remove it. + + + + + + + Fix contrib/seg to print segments + with ~ certainty indicators correctly + (Ewan Young) + § + + + + Due to a typo, seg_out() did not print + a ~ certainty indicator attached to a segment's + upper boundary. Worse, if the lower boundary + had ~ while the upper boundary had no indicator, + the upper boundary was not printed at all, incorrectly converting + the value into an open interval. + + + + + + + Fix crash with namespace nodes in contrib/xml2's + xpath_nodeset() function (Andrey Chernyy, + Michael Paquier) + § + + + + + + + Support building PostgreSQL with Visual + Studio 2026 (Andrew Dunstan) + + + + + + + Support building PostgreSQL with + OpenSSL 4 (Daniel Gustafsson) + § + + + + + + + Update time zone data files to tzdata + release 2026c (Tom Lane) + § + + + + Alberta (America/Edmonton) will be on year-round UTC-06 + (effectively, permanent DST) beginning in November 2026. This + release assumes that their TZ abbreviation will + be CST from that time forward. That seems likely + to change, but it's unclear what new abbreviation will be used. + + + + Morocco (Africa/Casablanca) will move to permanent UTC+00, + without daylight saving time transitions, on 2026-09-20. + + + + + + + + Release 18.4 From a4c41bbcc7b0aed721950303338d75b09eddc295 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 9 Aug 2026 12:52:47 -0400 Subject: [PATCH 202/250] Release notes for 18.5, 17.11, 16.15, 15.19, 14.24. --- doc/src/sgml/release-18.sgml | 54 ------------------------------------ 1 file changed, 54 deletions(-) diff --git a/doc/src/sgml/release-18.sgml b/doc/src/sgml/release-18.sgml index e2e3cf237f2..5cb5dd154c5 100644 --- a/doc/src/sgml/release-18.sgml +++ b/doc/src/sgml/release-18.sgml @@ -84,25 +84,6 @@ WHERE t.relhasindex AND ic.relam = 2742; - - Fix self-deadlock when replaying WAL generated by an older minor - version (Andrey Borodin) - - - - This error was introduced in the previous set of minor releases. It - caused standby servers that were following a primary of an older - minor release version to get stuck in some scenarios. - - - - - - - Treat an undefined jsonpath variable as an error even when no - variables are supplied (Andrey Rachitskiy) - - - - The jsonb @? - and @@ operators cannot supply any variables to - be used in their jsonpath expressions. This code path erroneously - treated an unknown jsonpath variable as a JSON null, rather than - raising an error as expected. Aside from not being the expected - behavior, this mistake could result in unbounded memory consumption. - - - - - - - Support building PostgreSQL with Visual - Studio 2026 (Andrew Dunstan) - - - - - + + Restrict logical decoding output plugins to the set specified by a + new server parameter output_plugin_libraries + (Jacob Champion) + § + § + + + + Previously, a replication user could select any loadable library for + logical decoding, allowing exploits of various sorts. To allow + locking this down without breaking setups that worked before, + introduce a whitelist of allowed output plugins. + + + + By default, only the output plugins shipped as part + of PostgreSQL + (pgoutput and test_decoding) + are included in output_plugin_libraries. + Installations that rely on other output plugins must add them + after updating the server, for example + +output_plugin_libraries = 'pgoutput, test_decoding, my_trusted_decoder' + + + + + Additionally, pg_upgrade --check will fail if the + output_plugin_libraries parameter on the new + cluster does not permit the plugins of logical replication slots + on the old cluster, when migrating from versions 17 and later. + Make necessary additions to the new cluster's setting before + performing pg_upgrade. + + + + The PostgreSQL Project thanks + Vladimir Tokarev and Yu Kunpeng + for reporting this problem. + (CVE-2026-6471) + + + + + + + Fix contrib/pgcrypto's PGP encryption to detect + unsupported ciphers (Daniel Gustafsson) + § + § + + + + Previously, if OpenSSL rejected the requested cipher (for example, + because it is running in FIPS mode, or the legacy provider hasn't + been loaded), pgcrypto failed to notice + the failure and simply XOR'd the non-encrypted block with the + plaintext, rendering the encryption trivially + breakable. This will typically occur with deprecated or non-FIPS + cipher algorithms (cipher-algo=blowfish/bf, twofish, cast5, or + 3des). + + + + By default, pgcrypto will now fail to + decrypt any messages that were affected in this way. To allow + retrieval of such data, a new + option ignore-cipher-failure has been added + to pgp_pub_decrypt() + and pgp_sym_decrypt(). + Setting ignore-cipher-failure=1 will restore + their previous behavior, allowing the faulty encryption wrapper to + be stripped off: + +pgp_sym_decrypt(encrypted_column, any key, 'ignore-cipher-failure=1') + + Once the affected messages are identified and stripped of their + wrappers, they can then be re-encrypted with a modern algorithm. It + is important however that the behavior of OpenSSL be the same as it + was when the faulty messages were created: if the set of unsupported + algorithms is not the same, this approach will not work. See the + documentation for ignore-cipher-failure. + + + + The PostgreSQL Project thanks + Shishir Sharma + for reporting this problem. + (CVE-2026-14663) + + + + + + + Fix psql to skip in-line data following a + scripted COPY ... FROM STDIN command, even if + the COPY fails before + sending PGRES_COPY_IN (Tom Lane) + § + § + + + + Previously, if a COPY command failed at startup + (for instance, because the target table doesn't exist) + psql would not realize that and would + proceed to read the following in-line data as SQL commands. In the + best case that's wrong and in the worst case it's a SQL-injection + hazard. Teach psql to recognize + syntactically-valid COPY ... FROM STDIN commands + and to skip data on its own authority if the server doesn't respond + with PGRES_COPY_IN. + + + + While this fix is unlikely to affect any production SQL scripts, + test scripts might intentionally exercise failing COPY + ... FROM STDIN commands. Those will need to gain + a \. data terminator line after each such + command. + + + + The PostgreSQL Project thanks + Alexander Lakhin + for reporting this problem. + (CVE-2026-6464) + + + + + + + Cross-check the output row type of a portal + running EXECUTE or FETCH + (Robert Haas) + § + + + + EXECUTE and FETCH use two + portals: an outer one for the statement itself, and an inner one + running the query being executed on its behalf. It was previously + possible to make the declared row types of the two portals diverge, + leading to server memory disclosure and arbitrary code execution. + + + + The PostgreSQL Project thanks + Ben Morris (in collaboration with Claude and Anthropic Research) + and Peter Geoghegan + for reporting this problem. + (CVE-2026-16239) + + + + + + + Fix buffer overrun with long time zone abbreviation in + to_char() (Tom Lane) + § + + + + This can easily crash the server, and exploits leading to arbitrary + code execution have been reported. + + + + The PostgreSQL Project thanks + Hcamael, Amjad Shahzad, Tan Zhen of AntAISecurityLab, Tomer Fichman, + Zheng Yu, Amy Burnett (OpenAI Codex Security), Rick de Jager, Heewon + Song, Sylvie Mayer, Aleksander Alekseev, and Hillai Ben Sasson + for reporting this problem. + (CVE-2026-14669) + + + + + + + Fix buffer overrun in regexp match/split functions (Masahiko Sawada) + § + + + + If passed invalidly-encoded data, these functions could write past + the end of their conversion buffer. + + + + The PostgreSQL Project thanks + Francesco Verardi + for reporting this problem. + (CVE-2026-14664) + + + + + + + Harden the ascii() function against invalid + input (Michael Paquier) + § + + + + By supplying invalidly-encoded input, this function could be coaxed + to read and return a few bytes of data that it shouldn't. In + assert-enabled builds, its assertions could be triggered too. + + + + The PostgreSQL Project thanks + Hcamael + for reporting this problem. + (CVE-2026-18024) + + + + + + + Fix multirange type handling + in pg_restore_attribute_stats() + (OpenAI Security Research Team) + § + § + + + + pg_restore_attribute_stats() treated multirange + types just like their underlying range type. This works correctly + for the bounds histogram, but it was wrong for all the other + statistics kinds. + + + + The PostgreSQL Project thanks + Amy Burnett (OpenAI Codex Security) + for reporting this problem. + (CVE-2026-16238) + + + + + + + Make scalarineqsel() check that a constant it + expects to be of type tid actually is (Tom Lane) + § + + + + This expectation will hold for all the built-in operators that use + this estimator, but a maliciously-constructed operator could violate + it, leading to a crash or server memory disclosure. + + + + The PostgreSQL Project thanks + Hcamael + for reporting this problem. + (CVE-2026-14668) + + + + + + + Harden tsvector and tsquery code against + overly long values (both individual lexemes and total vector/query + length) (Tom Lane) + § + § + + + + The documented limits were not enforced in all code paths. + + + + The PostgreSQL Project thanks + Yuhang Wu, Zhenpeng Lin, Zheng Yu, and Hcamael + for reporting these problems. + (CVE-2026-14662) + + + + + + + Fix various places that mistakenly assumed they would not have to + deal with more than FUNC_MAX_ARGS function + arguments (Tom Lane) + § + § + + + + Notably, the server's actual limit on the number of arguments to an + aggregate function is FUNC_MAX_ARGS - 1, but the + parser failed to enforce that, creating hazards downstream. + + + + The PostgreSQL Project thanks + Zheng Yu, ylwangtju, and Masahiko Sawada + for reporting these problems. + (CVE-2026-14679) + + + + + + + Reject calls from SQL to functions that take or return + type internal (Tom Lane) + § + § + + + + The existing defenses against doing this have been shown to be + insufficient, so add more explicit checks. + + + + The PostgreSQL Project thanks + Amy Burnett (OpenAI Codex Security) + for reporting this problem. + (CVE-2026-14680) + + + + + + + Preserve the ownership of extended statistics objects when they are + rebuilt by ALTER TABLE (Masahiko Sawada) + § + + + + Previously, the role running ALTER TABLE gained + ownership of such objects, but that seems inappropriate. + + + + The PostgreSQL Project thanks + Noah Misch + for reporting this problem. + (CVE-2026-6469) + + + + + + + When deparsing an EXTRACT() function call, + quote the field name if needed (Nathan Bossart) + § + + + + The parser accepts any string literal as a field name + in EXTRACT(), deferring validation to + execution. If the call is stored and deparsed (for example + during pg_dump), the string body was + regurgitated verbatim, allowing SQL injection. + + + + The PostgreSQL Project thanks + Ben Morris (in collaboration with Claude and Anthropic Research) + for reporting this problem. + (CVE-2026-15741) + + + + + + + Check for USAGE privilege on data types in places + that formerly failed to check that (Nathan Bossart) + § + § + § + + + + CREATE TYPE AS RANGE did not check, nor + did ALTER TABLE OF, nor did commands that create + stored expressions. These omissions allowed roles + without USAGE privilege to nonetheless create + objects depending on the type, possibly blocking the type's owner + from changing the type later. + + + + The PostgreSQL Project thanks + Jingzhou Fu + for reporting this problem. + (CVE-2026-6470) + + + + + + + Invalidate role-dependent cached plans after role changes + (Ilya Staroverov, Shinya Kato, Nathan Bossart) + § + + + + Role membership, role attribute, and database ownership changes may + impact the expected behavior of row-level security policies, but + previously we'd continue to use cached plans that were made + according to the old state of affairs. + + + + The PostgreSQL Project thanks + Ilya Staroverov and Shinya Kato + for reporting this problem. + (CVE-2026-14666) + + + + + + + Reject GSSEncRequest after direct SSL connection (Michael Paquier) + § + + + + After establishing a TLS-encrypted connection, the server would + still accept a request for GSSAPI encryption. If that succeeded, + the connection would proceed using TLS encryption, but it would look + like a GSS connection to the pg_hba rules. + Thus, a pg_hba policy intending to disallow TLS + would not be enforced correctly. + + + + The PostgreSQL Project thanks + p4p3r + for reporting this problem. + (CVE-2026-14681) + + + + + + + Make mock SCRAM authentication secrets more plausible (Nathan Bossart) + § + + + + If a SCRAM login is attempted against a role that doesn't exist or + doesn't have a SCRAM secret, we generate a mock secret and carry out + the authentication handshake anyway, to avoid revealing these facts + to an attacker. But the mock secret was made with a fixed iteration + count, which in itself can be an observable response discrepancy. + Use the configuration setting scram_iterations + instead, to make the mock secret look more like the installation's + real secrets. + + + + The PostgreSQL Project thanks + Radim Marek + for reporting this problem. + (CVE-2026-14672) + + + + + + + Fix out-of-bounds writes in ecpg + applications caused by invalid bytea data received from + the server (Michael Paquier) + § + + + + ecpg assumed without checking that + any bytea value must begin with \x. + A broken or malicious server might send a string shorter than 2 + bytes, resulting in memory clobber in the application. + + + + The PostgreSQL Project thanks + ylwangtju + for reporting this problem. + (CVE-2026-16241) + + + + + + + Do not do backquote expansion on the argument + of psql's \unrestrict + command (Nathan Bossart) + § + + + + This oversight in the fix for CVE-2025-8714 allows a malicious + server to inject shell commands into plain-text dump output that + will be run at restore time on the machine + running psql, the exact scenario that + CVE-2025-8714 intended to prevent. + + + + The PostgreSQL Project thanks + Lucas Velgus, Filip Janus, and Daniel Bakker + for reporting this problem. + (CVE-2026-18408) + + + + + + + Remove pg_dump's assumption that + pg_proc.protrftypes + cannot have more than FUNC_MAX_ARGS entries + (Tom Lane) + § + + + + Since there could be entries for both input and output arguments, + it's feasible for this array's length to exceed + FUNC_MAX_ARGS (which constrains only input + arguments). Even if that were not so, + pg_dump cannot assume that the server was + built with the same value of FUNC_MAX_ARGS that + it has. An overrun would lead to a memory clobber + inside pg_dump. + + + + The PostgreSQL Project thanks + Masahiko Sawada + for reporting this problem. + (CVE-2026-19385) + + + + + + + Harden PL/Perl + against tied Perl arrays and hashes (Tom Lane) + § + + + + A tied object that doesn't behave like a regular one could lead to + memory overwrite, or to constructing a corrupt result array (which + would likely cause problems later). + + + + The PostgreSQL Project thanks + Hcamael + for reporting this problem. + (CVE-2026-14670) + + + + + + + Fix integer overflows in memory-allocation calculations + in PL/Perl + and PL/Tcl (Heikki Linnakangas) + § + + + + This is the same type of problem as CVE-2026-6473, just in a + different part of the code, and is fixed in the same way. + + + + The PostgreSQL Project thanks + the Tulya Project (Team Dhiutsa, Bitecope Technologies Private Ltd) + for reporting this problem. + (CVE-2026-14677) + + + + + + + Ensure that contrib/amcheck functions + restrict search_path before executing index + expressions (Noah Misch) + § + + + + Because amcheck will run such index expressions as the owner of + their tables, a caller could potentially + hijack search_path-dependent functions to run + arbitrary code as the table owner. By default this is not a + vulnerability because only superusers are allowed to call amcheck + functions; but if that privilege was granted out, it created a + larger hazard than the documentation suggests. + + + + The PostgreSQL Project thanks + Yuelin Wang and Jacob Brazeal + for reporting this problem. + (CVE-2026-14673) + + + + + + + Fix integer overflows in contrib/fuzzystrmatch's + levenshtein() + and levenshtein_less_equal() functions + (Nathan Bossart) + § + + + + Passing large cost values to these functions could cause integer + overflows, thereby producing nonsensical results, and even causing + out-of-bounds writes in some cases. + + + + The PostgreSQL Project thanks + Ben Morris (in collaboration with Claude and Anthropic Research) + for reporting this problem. + (CVE-2026-15742) + + + + + + + Fix buffer overrun + in contrib/pg_stat_statements (Álvaro Herrera) + § + + + + Query normalization didn't accurately account for the amount of + space the normalized string would require. + + + + The PostgreSQL Project thanks + Sajeeb Lohani (with TrendAI Zero Day Initiative) and Yuelin Wang + for reporting this problem. + (CVE-2026-14676) + + + + + + + Fix datatype error in contrib/pg_trgm's GiST + picksplit function (Heikki Linnakangas) + § + + + + This mistake resulted in reading past the end of the buffer, + typically causing bad split decisions; but a crash could ensue + if you're very unlucky. + + + + The PostgreSQL Project thanks + Mehmet D. Ince + for reporting this problem. + (CVE-2026-14678) + + + + + + + Remove the plan cache in contrib/refint + (Ayush Tiwari) + § + + + + This caching behavior has several serious bugs, notably that + check_foreign_key() embeds the new key values + in its cascade-UPDATE queries, so a cached plan reuses the + originally-needed values rather than the key values that should be + used. The simplest solution is to remove it. + + + + The PostgreSQL Project thanks + Hcamael + for reporting this problem. + (CVE-2026-14671) + + + + + - - Remove the plan cache in contrib/refint - (Ayush Tiwari) - § - - - - This caching behavior has several serious bugs, notably that - check_foreign_key() embeds the new key values - in its cascade-UPDATE queries, so a cached plan reuses the - originally-needed values rather than the key values that should be - used. The simplest solution is to remove it. - - - - - - - Release 18.5 + + Release 18.6 Release date: @@ -15,8 +15,13 @@ . - - Migration to Version 18.5 + + Note: 18.5 was never released, due to a regression discovered + post-wrap. + + + + Migration to Version 18.6 A dump/restore is not required for those running 18.X. @@ -45,7 +50,7 @@ - + Changes @@ -1618,11 +1623,14 @@ Branch: REL_17_STABLE [8acfaa12a] 2026-08-05 11:40:39 +0200 Branch: REL_16_STABLE [b594efe52] 2026-08-05 11:40:39 +0200 Branch: REL_15_STABLE [0de744cd5] 2026-08-05 11:40:39 +0200 Branch: REL_14_STABLE [49a712f32] 2026-08-05 11:40:39 +0200 +Author: Heikki Linnakangas +Branch: REL_18_STABLE [5f003855e] 2026-08-11 21:24:28 +0300 --> Fix matching of localized month/day names in to_date() (Heikki Linnakangas) § + § From 724edf9bde9d356724ad384a2e196edc3c9f80f7 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 11 Aug 2026 14:38:31 -0400 Subject: [PATCH 245/250] Stamp 18.6. --- configure | 18 +++++++++--------- configure.ac | 2 +- meson.build | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/configure b/configure index 82684205bb9..6cef1a552e5 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 18.5. +# Generated by GNU Autoconf 2.69 for PostgreSQL 18.6. # # Report bugs to . # @@ -582,8 +582,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='PostgreSQL' PACKAGE_TARNAME='postgresql' -PACKAGE_VERSION='18.5' -PACKAGE_STRING='PostgreSQL 18.5' +PACKAGE_VERSION='18.6' +PACKAGE_STRING='PostgreSQL 18.6' 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 18.5 to adapt to many kinds of systems. +\`configure' configures PostgreSQL 18.6 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 18.5:";; + short | recursive ) echo "Configuration of PostgreSQL 18.6:";; 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 18.5 +PostgreSQL configure 18.6 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 18.5, which was +It was created by PostgreSQL $as_me 18.6, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ @@ -20179,7 +20179,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 18.5, which was +This file was extended by PostgreSQL $as_me 18.6, which was generated by GNU Autoconf 2.69. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -20250,7 +20250,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 18.5 +PostgreSQL config.status 18.6 configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" diff --git a/configure.ac b/configure.ac index 194ec778d8e..527437e0d20 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], [18.5], [pgsql-bugs@lists.postgresql.org], [], [https://www.postgresql.org/]) +AC_INIT([PostgreSQL], [18.6], [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/meson.build b/meson.build index e9356dfed73..707b222e05c 100644 --- a/meson.build +++ b/meson.build @@ -8,7 +8,7 @@ project('postgresql', ['c'], - version: '18.5', + version: '18.6', license: 'PostgreSQL', # We want < 0.56 for python 3.5 compatibility on old platforms. EPEL for From 394d02a65d12a31e962d571b0e11dcee884548b8 Mon Sep 17 00:00:00 2001 From: Noboru Saito Date: Thu, 13 Aug 2026 22:47:25 +0900 Subject: [PATCH 246/250] =?UTF-8?q?18.6=E7=BF=BB=E8=A8=B3=E6=BA=96?= =?UTF-8?q?=E5=82=991=20replace=E3=81=BE=E3=81=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/src/sgml/amcheck.sgml | 1 - doc/src/sgml/config.sgml | 21 ++------------------- doc/src/sgml/contrib-spi.sgml | 7 ------- doc/src/sgml/dblink.sgml | 6 ------ doc/src/sgml/ddl.sgml | 1 - doc/src/sgml/ecpg.sgml | 7 ------- doc/src/sgml/func.sgml | 5 ++--- doc/src/sgml/logical-replication.sgml | 1 + doc/src/sgml/maintenance.sgml | 17 +---------------- doc/src/sgml/postgres-fdw.sgml | 5 ----- doc/src/sgml/ref/alter_table.sgml | 8 -------- doc/src/sgml/ref/drop_subscription.sgml | 1 - doc/src/sgml/ref/pg_recvlogical.sgml | 3 --- doc/src/sgml/ref/psql-ref.sgml | 1 - doc/src/sgml/release-18.sgml | 13 +++++++++---- doc/src/sgml/storage.sgml | 17 ----------------- doc/src/sgml/stylesheet-speedup-xhtml.xsl | 7 ------- 17 files changed, 15 insertions(+), 106 deletions(-) diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml index cc34e19a864..791687b2db8 100644 --- a/doc/src/sgml/amcheck.sgml +++ b/doc/src/sgml/amcheck.sgml @@ -585,7 +585,6 @@ B-Tree検証関数のheapallindexed引数がtrue <filename>amcheck</filename>を効果的に使う - -ECDHキー交換で使われる曲線の名前を指定します。 -接続するすべてのクライアントがこの設定をサポートしている必要があります。 -コロンで区切られたリストを使用して複数の曲線を指定できます。 -サーバの楕円曲線キーで使用されるのと同じ曲線である必要はありません。 -このパラメータは、postgresql.confファイルか、サーバのコマンドラインでのみ設定可能です。 -デフォルト値はX25519:prime256v1です。 @@ -2356,10 +2349,6 @@ TLSバージョン1.3を使用する接続についてはがオンの時やベースバックアップ中です)。 -圧縮されたページイメージはWAL再生中に伸長されます。 -サポートされている方法はpglzlz4PostgreSQLでコンパイルされた場合)およびzstdPostgreSQLでコンパイルされた場合)です。 -デフォルト値はoffです。 -スーパーユーザと適切なSET権限を持つユーザのみがこの設定を変更できます。 +《》 @@ -12166,7 +12150,6 @@ log_line_prefix = '%m [%p] %q%u@%d/%a ' - -check_foreign_key()は被参照テーブルを検査します。 -使用方法は、この関数を使用するAFTER DELETE OR UPDATEトリガを他のテーブルで参照されるテーブルに作成することです。 -トリガ引数は、この関数が検査を実行しなければならない参照テーブル数、参照キーが見つかった場合の動作(cascade — 参照行を削除、restrict — 参照キーが存在する場合トランザクションをアボート、setnull —参照キーフィールドをNULLに設定)、主/一意キーを形成するトリガを発行したテーブルの列名、参照テーブルの名前と列名(最初の引数で指定された数のテーブル分繰り返す)です。 -主/一意キー列はNOT NULLと指定されていなければならず、また、一意性インデックスを持つべきであることに注意してください。 diff --git a/doc/src/sgml/dblink.sgml b/doc/src/sgml/dblink.sgml index 046922a1e90..4d53241bd88 100644 --- a/doc/src/sgml/dblink.sgml +++ b/doc/src/sgml/dblink.sgml @@ -218,7 +218,6 @@ dblink_connect(text connname, text connstr) returns text - -外部データラッパーdblink_fdwには、追加のブールオプションuse_scram_passthroughがあり、dblinkがSCRAMパススルー認証を使用してリモートデータベースに接続するかどうかを制御します。 -SCRAMパススルー認証では、dblinkはプレーンテキストユーザパスワードの代わりにSCRAMハッシュ化されたシークレットを使用してリモートサーバに接続します。 -これにより、プレーンテキストユーザパスワードがPostgreSQLシステムカタログに格納されるのを回避します。 -詳細と制限については、postgres_fdwの相当するuse_scram_passthroughオプションの文書を参照してください。 diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 70ae8f6c1de..b55a71c578f 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -3193,7 +3193,6 @@ REVOKE ALL ON accounts FROM PUBLIC; REFERENCES - -このコマンドには2つの構文があります。 -1番目の構文では、そのまま結果セットに適用されている記述子のヘッダ項目を取り出します。 -行数が1つの例です。 -列番号を追加のパラメータとして必要とする2番目の構文では特定の列に関する情報を取り出します。 -例えば、列名と列の実際の値です。 diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 3dffdf07dce..39bec36f5a1 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -6621,7 +6621,7 @@ cast(-1234 as bytea) \xfffffb2e Bitwise shift left (string length is preserved) --> -ビット単位の左シフト(文字列長は保存されます) +《》 B'10001' << 3 @@ -6639,7 +6639,7 @@ cast(-1234 as bytea) \xfffffb2e Bitwise shift right (string length is preserved) --> -ビット単位の右シフト(文字列長は保存されます) +《》 B'10001' >> 2 @@ -18972,7 +18972,6 @@ OIDで指定したパーサが認識できるトークンの型を記述する uuid - -古いプリペアドトランザクションを解決します。 -pg_prepared_xactsのage(transactionid)が大きい行を確認して見つけることができます。 -このようなトランザクションはコミットまたはロールバックされるべきです。 - -長時間実行されているオープントランザクションを終了します。 -pg_stat_activityでage(backend_xid)またはage(backend_xmin)が大きい行を確認して、これらを見つけることができます。 -このようなトランザクションはコミットまたはロールバックするか、pg_terminate_backendを使用してセッションを終了できます。 - -古いレプリケーションスロットを削除します。 -pg_stat_replicationを使用してage(xmin)またはage(catalog_xmin)が大きいスロットを見つけます。 -多くの場合、そのようなスロットは、もはや存在しないか長い間ダウンしているサーバへのレプリケーションのために作成されたものです。 -存在するサーバに対してスロットを削除しても、そのスロットに接続しようとする可能性がある場合、そのレプリカは再構築する必要があるでしょう。 -このオプションは、postgres_fdwがSCRAMパススルー認証を使用して外部サーバに接続するかどうかを制御します。 -SCRAMパススルー認証では、postgres_fdwはプレーンテキストユーザパスワードの代わりにSCRAMハッシュ化されたシークレットを使用してリモートサーバに接続します。 -これにより、プレーンテキストユーザパスワードがPostgreSQLシステムカタログに格納されるのを回避します。 diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index d284021ea39..396824fe514 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -1451,7 +1451,6 @@ fillfactor、TOAST、およびautovacuumのストレージパラメータおよ ATTACH PARTITION partition_name { FOR VALUES partition_bound_spec | DEFAULT } - -この構文は、既存のテーブル(それ自体がパーティションテーブルのこともあります)を対象テーブルのパーティションとして追加します。 -テーブルは、FOR VALUESを使って指定の値のパーティションとして、あるいは、DEFAULTを使ってデフォルトパーティションとして追加できます。 -対象テーブルの各インデックスについて、対応するインデックスが付加されるテーブルに作られます。 -また、同等のインデックスが既にある場合には、そのインデックスが、ALTER INDEX ATTACH PARTITIONが実行された場合と同様に、対象テーブルのインデックスに付加されます。 -既存のテーブルが外部テーブルの場合、今のところ対象テーブルにUNIQUEインデックスがあるときにはテーブルを対象テーブルのパーティションとして追加することはできない点に注意してください(も参照してください)。 -対象テーブルにある各ユーザ定義の行レベルのトリガに対しては、対応するものが付加されるテーブルに作られます。 diff --git a/doc/src/sgml/ref/drop_subscription.sgml b/doc/src/sgml/ref/drop_subscription.sgml index 055ced93fbf..b47d14e4d4a 100644 --- a/doc/src/sgml/ref/drop_subscription.sgml +++ b/doc/src/sgml/ref/drop_subscription.sgml @@ -122,7 +122,6 @@ DROP SUBSCRIPTION [ IF EXISTS ] name注釈 - -pg_recvlogicalは、ソースクラスタでグループパーミッションが有効である場合、受け取ったWALファイルのグループパーミッションを維持します。 diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index f375ebc86f2..1905f0ea475 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -3944,7 +3944,6 @@ SELECT \l[x+]または\list[x+] [ pattern ] - + リリース日: 2026-08-13 @@ -24,7 +27,10 @@ Migration to Version 18.6 + +18.Xからの移行ではダンプ/リストアは不要です。 @@ -51,7 +57,10 @@ + + 変更点 @@ -4087,15 +4096,11 @@ Branch: REL_14_STABLE [b282280e9] 2026-05-11 05:13:51 -0700 - -パスワードやハッシュなどの検証には、 memcpy()strcmp()の代わりにtimingsafe_bcmp()を使用するようになりました。 -これらの関数のデータ依存性が、これらの箇所で悪用される可能性があるかどうかは不明ですが、安全を期してこれらが置き換えられました。 diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml index 27fba02d8e1..103cc49cf96 100644 --- a/doc/src/sgml/storage.sgml +++ b/doc/src/sgml/storage.sgml @@ -1239,7 +1239,6 @@ data. Empty in ordinary tables. - -それぞれのページの最初の24バイトはページヘッダ(PageHeaderData)から構成されています。 -その書式をにて説明します。 -最初のフィールドは、このページに関連する最も最近のWAL項目を表しています。 -2番目のフィールドにはが有効な場合にページチェックサムが格納されています。 -次にフラグビットを含む2バイトのフィールドがあります。 -その後に2バイトの整数フィールドが3つ続きます(pd_lowerpd_upperpd_special)。 -これらには、割り当てられていない空間の始まり、割り当てられていない空間の終わり、そして特別な空間の始まりのバイトオフセットが格納されています。 -ページヘッダの次の2バイトであるpd_pagesize_versionは、ページサイズとバージョン指示子の両方を格納します。 -PostgreSQL 8.3以降のバージョン番号は4、PostgreSQL 8.1と8.2のバージョン番号は3、PostgreSQL 8.0のバージョン番号は2、PostgreSQL 7.3と7.4のバージョン番号は1です。 -それより前のリリースのバージョン番号は0です。 -(ほとんどのバージョン間で基本的なページレイアウトやヘッダの書式は変更されていませんが、ヒープ行ヘッダのレイアウトが変更されました。) -ページサイズは基本的に照合用としてのみ存在しています。 -同一インストレーションでの複数のページサイズはサポートされていません。 -最後のフィールドはそのページの切り詰めが有益かどうかを示すヒントです。 -これはページ上で切り詰められていないもっとも古いXMAXが追跡するものです。 diff --git a/doc/src/sgml/stylesheet-speedup-xhtml.xsl b/doc/src/sgml/stylesheet-speedup-xhtml.xsl index 3befd1ee082..0e8e97c7e5c 100644 --- a/doc/src/sgml/stylesheet-speedup-xhtml.xsl +++ b/doc/src/sgml/stylesheet-speedup-xhtml.xsl @@ -262,7 +262,6 @@ - - - - - - From 97adc61fa3f4e35b651b0ee7e1b9853110a24993 Mon Sep 17 00:00:00 2001 From: Noboru Saito Date: Thu, 13 Aug 2026 23:06:12 +0900 Subject: [PATCH 247/250] =?UTF-8?q?18.6=E7=BF=BB=E8=A8=B3=E6=BA=96?= =?UTF-8?q?=E5=82=992=20=E9=A1=9E=E4=BC=BC=E5=88=86=E6=8C=BF=E5=85=A5?= =?UTF-8?q?=E6=A9=9F=E6=A2=B0=E7=BF=BB=E8=A8=B3=E6=BA=96=E5=82=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/src/sgml/amcheck.sgml | 5 + doc/src/sgml/config.sgml | 58 +- doc/src/sgml/contrib-spi.sgml | 20 + doc/src/sgml/dblink.sgml | 7 + doc/src/sgml/ddl.sgml | 6 + doc/src/sgml/ecpg.sgml | 8 + doc/src/sgml/func.sgml | 9 +- doc/src/sgml/logical-replication.sgml | 6 + doc/src/sgml/maintenance.sgml | 22 + doc/src/sgml/oauth-validators.sgml | 5 + doc/src/sgml/pgcrypto.sgml | 9 + doc/src/sgml/postgres-fdw.sgml | 6 + doc/src/sgml/ref/alter_table.sgml | 9 + doc/src/sgml/ref/copy.sgml | 4 + doc/src/sgml/ref/create_type.sgml | 4 + doc/src/sgml/ref/drop_subscription.sgml | 3 + doc/src/sgml/ref/pg_recvlogical.sgml | 4 + doc/src/sgml/ref/psql-ref.sgml | 12 + doc/src/sgml/release-18.sgml | 1101 ++++++++++++++++++++++- doc/src/sgml/storage.sgml | 17 + 20 files changed, 1304 insertions(+), 11 deletions(-) diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml index 791687b2db8..65ebc972db3 100644 --- a/doc/src/sgml/amcheck.sgml +++ b/doc/src/sgml/amcheck.sgml @@ -585,9 +585,14 @@ B-Tree検証関数のheapallindexed引数がtrue <filename>amcheck</filename>を効果的に使う + +《マッチ度[70.621469]》amcheckは、データチェックサムが検知できないような、様々なタイプの障害モードを効果的に検知できます。 +以下のようなものがあります。 +《機械翻訳》«amcheck can be effective at detecting various types of failure modes that data checksums will fail to catch. These include:» diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 1beb86a13bb..0ed4e477f55 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1756,6 +1756,7 @@ SCRAM-SHA-256を使用してパスワードを暗号化するときに実行さ + +《機械翻訳》«If a role password was created with a different iteration count than the value of scram_iterations specified in the postgresql.conf file or on the server command line, an unauthenticated user can discern the existence of the role by observing discrepancies in the server's responses to connection attempts. If you find this concerning, ensure that all role passwords are created with scram_iterations set to the value specified in the postgresql.conf file or on the server command line.» @@ -2330,6 +2333,7 @@ TLSバージョン1.3を使用する接続についてはpostgresql.conf file or on the server command line. The default is X25519:prime256v1. +--> +《マッチ度[74.065421]》ECDHキー交換で使われる曲線の名前を指定します。 +接続するすべてのクライアントがこの設定をサポートしている必要があります。 +コロンで区切られたリストを使用して複数の曲線を指定できます。 +サーバの楕円曲線キーで使用されるのと同じ曲線である必要はありません。 +このパラメータは、postgresql.confファイルか、サーバのコマンドラインでのみ設定可能です。 +デフォルト値はX25519:prime256v1です。 +《機械翻訳》«Specifies the named group to use for TLS key exchange. It needs to be supported by all clients that connect. Multiple groups can be specified by using a colon-separated list. It does not need to match the key type used by the server certificate. This parameter can only be set in the postgresql.conf file or on the server command line. The default is X25519:prime256v1 + +《マッチ度[82.232346]》最も一般的な曲線のOpenSSL名は、prime256v1(NIST P-256)、secp384r1(NIST P-384)、およびsecp521r1(NIST P-521)です。 +openssl ecparam -list_curvesコマンドを使用すると、使用可能なグループの不完全なリストを表示することができます。 +ただし、すべてがTLSで使用できるわけではなく、サポートされているグループ名と別名の多くは省略されています。 +《機械翻訳》«OpenSSL names for the most common groups are: prime256v1 (NIST P-256), secp384r1 (NIST P-384), secp521r1 (NIST P-521). An incomplete list of available groups can be shown with the command openssl ecparam -list_curves. Not all of them are usable with TLS though, and many supported group names and aliases are omitted.» @@ -5210,15 +5228,21 @@ WALの更新をディスクへ強制するのに使用される方法です。 A compressed page image will be decompressed during WAL replay. The supported methods are pglz, lz4 (if PostgreSQL - was compiled with ) and + was compiled with ) and zstd (if PostgreSQL - was compiled with ). + was compiled with ). The value on is a historical spelling of pglz. The default value is off. Only superusers and users with the appropriate SET privilege can change this setting. --> -《》 +《マッチ度[77.751756]》このパラメータは、指定された圧縮方式を使用したWALの圧縮を有効にします。 +有効にすると、PostgreSQLサーバはWALに書き込まれる全ページイメージを圧縮します(例えば、がオンの時やベースバックアップ中です)。 +圧縮されたページイメージはWAL再生中に伸長されます。 +サポートされている方法はpglzlz4PostgreSQLでコンパイルされた場合)およびzstdPostgreSQLでコンパイルされた場合)です。 +デフォルト値はoffです。 +スーパーユーザと適切なSET権限を持つユーザのみがこの設定を変更できます。 +《機械翻訳》«This parameter enables compression of WAL using the specified compression method. When enabled, the PostgreSQL server compresses full page images written to WAL (e.g. when is on, during a base backup, etc.). A compressed page image will be decompressed during WAL replay. The supported methods are pglz, lz4 (if PostgreSQL was compiled with ) and zstd (if PostgreSQL was compiled with ). The value on is a historical spelling of pglz. The default value is off. Only superusers and users with the appropriate SET privilege can change this setting.» @@ -6926,6 +6950,7 @@ WAL要約は、先行するバックアップと新しいバックアップの + +《機械翻訳》«Lists the libraries installed in that are also trusted for use as logical output plugins by replication clients. Any logical decoding or replication requests for other libraries will be refused. All users are subject to this restriction. The default is 'pgoutput, test_decoding', which are the two logical output plugins included in the standard PostgreSQL distribution.» + +《機械翻訳》«The format is a comma-separated list of library names, where each name is interpreted as for the LOAD command (but logical decoding clients must specify a plugin name that exactly matches an entry in the list, without variations in case or path structure). Whitespace between entries is ignored; surround a library name with double quotes if you need to include whitespace or commas in the name.» + +《機械翻訳》«It is the responsibility of the server administrator to ensure that libraries added to this list do not unintentionally give additional privileges to non-superusers when they are loaded into the server.» + +《機械翻訳》«When updating the server from a version that does not have the output_plugin_libraries parameter, the following query can help construct the list of plugins that are required by all persistent logical replication slots:» SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL; + +《機械翻訳》«Review the list carefully for safety before adjusting output_plugin_libraries + +《機械翻訳》«The above query can only display plugins which were successfully added to replication slots at some point in the past. Newly refused requests will appear in the logs with a message similar to» ERROR: library "..." may not be used as an output plugin DETAIL: The configuration parameter "output_plugin_libraries" (currently 'pgoutput, test_decoding') does not name this library as a trusted output plugin. @@ -12150,12 +12192,18 @@ log_line_prefix = '%m [%p] %q%u@%d/%a ' + +《マッチ度[82.594937]》この設定は、および関連設定の結果を表示するログメッセージにのみ影響します。 +ゼロ以外の値の設定は、とりわけパラメータがバイナリ形式で送信される際に多少のオーバーヘッドをもたらします。 +テキストへの変換が必要になるからです。 +《機械翻訳》«This setting only affects log messages printed as a result of , , and related settings. Non-zero values of this setting add some overhead, particularly if parameters are sent in binary form, since then conversion to text is required.» @@ -19055,7 +19103,11 @@ LOG: CleanUpLock: deleting: lock(0xb7acd844) id(24688,24696,0,0,0,1) + +《マッチ度[63.291139]》が有効の時のみ効果があります。 +《機械翻訳》«Only has effect if data checksums are enabled.» +《マッチ度[75.218659]》check_primary_key()およびcheck_foreign_key()は、外部キー制約を検査するために使用されます。 +(当然ながら、この機能はかなり前に組み込みの外部キー機能に取って代わりました。しかし例としてはまだ有用です。) +《機械翻訳》«check_primary_key() and check_foreign_key() are used to check foreign key constraints. (This functionality is long since superseded by the built-in foreign key mechanism, of course, but the module is still useful as an example. This module will be removed in PostgreSQL 20.)» + +《機械翻訳》«refint requires a secure schema usage pattern and data types where the equality operator is named = @@ -79,6 +87,7 @@ + +《機械翻訳》«The referenced table name and column name arguments to check_primary_key() are copied as-is into internally generated SQL statements and therefore must be double-quoted by the user as necessary in the CREATE TRIGGER command. See for more information about quoting SQL identifiers. Conversely, the referencing table column name arguments should not be double quoted. See the following mock example of proper use of check_primary_key() CREATE TRIGGER mytrigger AFTER INSERT OR UPDATE ON referencing_table @@ -101,6 +112,7 @@ check_primary_key ( + +《マッチ度[94.533030]》check_foreign_key()は被参照テーブルを検査します。 +使用方法は、この関数を使用するAFTER DELETE OR UPDATEトリガを他のテーブルで参照されるテーブルに作成することです。 +トリガ引数は、この関数が検査を実行しなければならない参照テーブル数、参照キーが見つかった場合の動作(cascade — 参照行を削除、restrict — 参照キーが存在する場合トランザクションをアボート、setnull —参照キーフィールドをNULLに設定)、主/一意キーを形成するトリガを発行したテーブルの列名、参照テーブルの名前と列名(最初の引数で指定された数のテーブル分繰り返す)です。 +主/一意キー列はNOT NULLと指定されていなければならず、また、一意性インデックスを持つべきであることに注意してください。 + +《機械翻訳》«The referencing table name and column name arguments to check_foreign_key() are copied as-is into internally generated SQL statements and therefore must be double-quoted by the user as necessary in the CREATE TRIGGER command. See for more information about quoting SQL identifiers. Conversely, the referenced table column name arguments should not be double quoted. See the following mock example of proper use of check_foreign_key() CREATE TRIGGER mytrigger AFTER DELETE OR UPDATE ON referenced_table diff --git a/doc/src/sgml/dblink.sgml b/doc/src/sgml/dblink.sgml index 4d53241bd88..e2337064b3b 100644 --- a/doc/src/sgml/dblink.sgml +++ b/doc/src/sgml/dblink.sgml @@ -218,6 +218,7 @@ dblink_connect(text connname, text connstr) returns text + +《マッチ度[80.746089]》外部データラッパーdblink_fdwには、追加のブールオプションuse_scram_passthroughがあり、dblinkがSCRAMパススルー認証を使用してリモートデータベースに接続するかどうかを制御します。 +SCRAMパススルー認証では、dblinkはプレーンテキストユーザパスワードの代わりにSCRAMハッシュ化されたシークレットを使用してリモートサーバに接続します。 +これにより、プレーンテキストユーザパスワードがPostgreSQLシステムカタログに格納されるのを回避します。 +詳細と制限については、postgres_fdwの相当するuse_scram_passthroughオプションの文書を参照してください。 +《機械翻訳》«The foreign-data wrapper dblink_fdw has an additional Boolean option use_scram_passthrough that controls whether dblink will use the SCRAM pass-through authentication to connect to the remote database. It can be specified for a foreign server or a user mapping. A user mapping setting overrides the foreign server setting. With SCRAM pass-through authentication, dblink uses SCRAM-hashed secrets instead of plain-text user passwords to connect to the remote server. This avoids storing plain-text user passwords in PostgreSQL system catalogs. See the documentation of the equivalent use_scram_passthrough option of postgres_fdw for further details and restrictions.» diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index b55a71c578f..5acae78469f 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -3193,12 +3193,15 @@ REVOKE ALL ON accounts FROM PUBLIC; REFERENCES + +《機械翻訳》«Allows creation of a foreign key constraint referencing a table, or specific column(s) of a table. Great care should be taken when granting this privilege, since a user who creates a foreign key can arrange for enforcement of that foreign key to call an arbitrary function, such as a cast function, and such functions will be called with the privileges of the table owner.» @@ -3207,9 +3210,12 @@ REVOKE ALL ON accounts FROM PUBLIC; TRIGGER + +《機械翻訳》«Allows creation of a trigger on a table, view, etc. Great care should be taken when granting this privilege, since any triggers added to a table or view will be executed with the privileges of users who modify it.» diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml index e454132f9ea..7a527cccb2f 100644 --- a/doc/src/sgml/ecpg.sgml +++ b/doc/src/sgml/ecpg.sgml @@ -10334,12 +10334,20 @@ GET DESCRIPTOR descriptor_name VALU + +《マッチ度[89.488636]》このコマンドには2つの構文があります。 +1番目の構文では、そのまま結果セットに適用されている記述子のヘッダ項目を取り出します。 +行数が1つの例です。 +列番号を追加のパラメータとして必要とする2番目の構文では特定の列に関する情報を取り出します。 +例えば、列名と列の実際の値です。 +《機械翻訳》«This command has two forms: The first form retrieves descriptor header item, which applies to the result set in its entirety. One example is the row count. The second form, which requires the column number as additional parameter, retrieves information about a particular column. Examples are the column name and the actual column value.» diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 39bec36f5a1..234d35f3439 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -6621,7 +6621,8 @@ cast(-1234 as bytea) \xfffffb2e Bitwise shift left (string length is preserved) --> -《》 +《マッチ度[65.957447]》ビット単位の左シフト(文字列長は保存されます) +《機械翻訳》«Bitwise shift left (string length is preserved)» B'10001' << 3 @@ -6639,7 +6640,8 @@ cast(-1234 as bytea) \xfffffb2e Bitwise shift right (string length is preserved) --> -《》 +《マッチ度[66.666667]》ビット単位の右シフト(文字列長は保存されます) +《機械翻訳》«Bitwise shift right (string length is preserved)» B'10001' >> 2 @@ -18972,6 +18974,7 @@ OIDで指定したパーサが認識できるトークンの型を記述する uuid + +《機械翻訳》«Generates a version 7 (time-ordered) UUID. The timestamp is computed using UNIX timestamp with millisecond precision + sub-millisecond timestamp + random. The optional parameter shift will shift the computed timestamp by the given interval. Infinite interval values are not accepted. The shifted timestamp must fall within the range supported by UUID version 7's 48-bit millisecond timestamp field: from 1970-01-01 00:00:00 UTC to approximately year 10889. An error is raised if the resulting timestamp is outside this range.» uuidv7() diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index aa6b6564902..c6a2741c23b 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -3268,12 +3268,15 @@ WAL送信プロセスはWALのロジカルデコーディング( + +《機械翻訳》«The name of the output plugin used by the replication connection must be included in the server's . (For subscriptions, the plugin name that is used is pgoutput.) Superusers may modify the trusted list per-connection, by including options=-coutput_plugin_libraries=... in the connection string.» @@ -3609,10 +3612,13 @@ WAL送信プロセスはWALのロジカルデコーディング( + +《機械翻訳》«The output plugins referenced by the slots in the old cluster must be installed in the new PostgreSQL executable directory. They must also be included in the new cluster's ; see that parameter's documentation for safety information.» diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 417857fff2d..95833edbbb2 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -1067,19 +1067,32 @@ HINT: Execute a database-wide VACUUM in that database. + +《マッチ度[82.068966]》古いプリペアドトランザクションを解決します。 +pg_prepared_xactsのage(transactionid)が大きい行を確認して見つけることができます。 +このようなトランザクションはコミットまたはロールバックされるべきです。 +《機械翻訳》«Resolve old prepared transactions. You can find these by checking pg_prepared_xacts for rows where age(transactionid) is large. Such transactions should be committed or rolled back.» + +《マッチ度[85.885167]》長時間実行されているオープントランザクションを終了します。 +pg_stat_activityでage(backend_xid)またはage(backend_xmin)が大きい行を確認して、これらを見つけることができます。 +このようなトランザクションはコミットまたはロールバックするか、pg_terminate_backendを使用してセッションを終了できます。 +《機械翻訳》«End long-running open transactions. You can find these by checking pg_stat_activity for rows where age(backend_xid) or age(backend_xmin) is large. Such transactions should be committed or rolled back, or the session can be terminated using pg_terminate_backend + +《マッチ度[81.102362]》古いレプリケーションスロットを削除します。 +pg_stat_replicationを使用してage(xmin)またはage(catalog_xmin)が大きいスロットを見つけます。 +多くの場合、そのようなスロットは、もはや存在しないか長い間ダウンしているサーバへのレプリケーションのために作成されたものです。 +存在するサーバに対してスロットを削除しても、そのスロットに接続しようとする可能性がある場合、そのレプリカは再構築する必要があるでしょう。 +《機械翻訳》«Drop any old replication slots. Use pg_replication_slots to find slots where age(xmin) or age(catalog_xmin) is large. In many cases, such slots were created for replication to servers that no longer exist, or that have been down for a long time. If you drop a slot for a server that still exists and might still try to connect to that slot, that replica may need to be rebuilt.» +《機械翻訳》«Unlike transaction ID wraparound, replication slots do not directly hold back multixact cleanup. Dropping stale replication slots is therefore not usually relevant to resolving multixact ID wraparound problems.» シャットダウンコールバック + +《マッチ度[78.723404]》shutdown_cbコールバックは、接続に関連付けられたバックエンドプロセスが終了するときに実行されます。 +検証器モジュールにメモリを割り当てられた状態がある場合、このコールバックはリソースリークを回避するためにフリーする必要があります。 +《機械翻訳》«The shutdown_cb callback is executed when the server backend has finished validating tokens for the connection. If the validator module has any allocated state, this callback should free it to avoid resource leaks.» typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state); diff --git a/doc/src/sgml/pgcrypto.sgml b/doc/src/sgml/pgcrypto.sgml index 0cae7c7dcf2..25b778bf58f 100644 --- a/doc/src/sgml/pgcrypto.sgml +++ b/doc/src/sgml/pgcrypto.sgml @@ -1531,6 +1531,7 @@ Applies to: pgp_sym_encrypt, pgp_pub_encrypt ignore-cipher-failure + +《機械翻訳》«Dangerous! Instructs pgcrypto to use an incorrect decryption algorithm matching the historical behavior prior to the fix for CVE-2026-14663, by completely ignoring failures from the OpenSSL cipher in use. This is intended only for users who need to recover incorrectly-encrypted messages created when the cipher-algo was unavailable under the OpenSSL configuration in use. Such faulty messages do not require the correct decryption key when ignore-cipher-failure is enabled, so there is no guarantee that the decrypted plaintext actually originated from a holder of the key.» + +《機械翻訳》«Contrast the case of a message which was correctly encrypted, but the cipher that produced it is unavailable under the current OpenSSL configuration. Recovering such plaintext via pgcrypto requires making the actual cipher available to OpenSSL by, for example, enabling the appropriate provider. ignore-cipher-failure is not necessary or helpful for that scenario. If decryption of a correctly encrypted message with this option happens to pass PGP integrity checks, that result is coincidental and does not make the recovered plaintext trustworthy.» Values: 0, 1 @@ -1972,9 +1978,12 @@ fips_mode() returns boolean fipsは、OpenSSLがFIPSモードで動作していることが検出された場合に、これらの関数を無効にします。 + +《機械翻訳》«pgp_sym_encrypt() and pgp_pub_encrypt() do not use built in crypto so they are not affected.» diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index 4617ed2f36e..ccdbdbd1a7d 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -1120,6 +1120,7 @@ check制約でそのような一貫しない動作があると、問い合わせ use_scram_passthrough (boolean) + +《マッチ度[68.297456]》このオプションは、postgres_fdwがSCRAMパススルー認証を使用して外部サーバに接続するかどうかを制御します。 +SCRAMパススルー認証では、postgres_fdwはプレーンテキストユーザパスワードの代わりにSCRAMハッシュ化されたシークレットを使用してリモートサーバに接続します。 +これにより、プレーンテキストユーザパスワードがPostgreSQLシステムカタログに格納されるのを回避します。 +《機械翻訳》«This option controls whether postgres_fdw will use the SCRAM pass-through authentication to connect to the foreign server. It can be specified for a foreign server or a user mapping. A user mapping setting overrides the foreign server setting. With SCRAM pass-through authentication, postgres_fdw uses SCRAM-hashed secrets instead of plain-text user passwords to connect to the remote server. This avoids storing plain-text user passwords in PostgreSQL system catalogs.» diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 396824fe514..18cdd0d88cf 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -1451,6 +1451,7 @@ fillfactor、TOAST、およびautovacuumのストレージパラメータおよ ATTACH PARTITION partition_name { FOR VALUES partition_bound_spec | DEFAULT } + +《マッチ度[72.660358]》この構文は、既存のテーブル(それ自体がパーティションテーブルのこともあります)を対象テーブルのパーティションとして追加します。 +テーブルは、FOR VALUESを使って指定の値のパーティションとして、あるいは、DEFAULTを使ってデフォルトパーティションとして追加できます。 +対象テーブルの各インデックスについて、対応するインデックスが付加されるテーブルに作られます。 +また、同等のインデックスが既にある場合には、そのインデックスが、ALTER INDEX ATTACH PARTITIONが実行された場合と同様に、対象テーブルのインデックスに付加されます。 +既存のテーブルが外部テーブルの場合、今のところ対象テーブルにUNIQUEインデックスがあるときにはテーブルを対象テーブルのパーティションとして追加することはできない点に注意してください(も参照してください)。 +対象テーブルにある各ユーザ定義の行レベルのトリガに対しては、対応するものが付加されるテーブルに作られます。 +《機械翻訳》«This form attaches an existing table (which might itself be partitioned) as a partition of the target table. The table can be attached as a partition for specific values using FOR VALUES or as a default partition by using DEFAULT. For each index in the target table, if a valid equivalent index already exists in the partition, it will be attached to the target table's index, as if ALTER INDEX ATTACH PARTITION had been executed; otherwise, a new corresponding index will be created. Invalid indexes on the partition are skipped. Note that if the existing table is a foreign table, it is currently not allowed to attach the table as a partition of the target table if there are UNIQUE indexes on the target table. (See also .) For each user-defined row-level trigger that exists in the target table, a corresponding one is created in the attached table.» diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index f5b1451b8c9..3d425e407d6 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -677,11 +677,15 @@ WHERE condition + +《マッチ度[86.379928]》今のところ、WHERE式の中での副問い合わせは認められていませんし、評価はCOPY自身により行われた変更を見ることはありません(これは、式がVOLATILE関数の呼び出しを含む場合に問題になります)。 +《機械翻訳》«Currently, subqueries and generated columns are not allowed in WHERE expressions, and the evaluation does not see any changes made by the COPY itself (this matters when the expression contains calls to VOLATILE functions).» diff --git a/doc/src/sgml/ref/create_type.sgml b/doc/src/sgml/ref/create_type.sgml index e32e76c0d3b..20e1885e706 100644 --- a/doc/src/sgml/ref/create_type.sgml +++ b/doc/src/sgml/ref/create_type.sgml @@ -271,8 +271,12 @@ CREATE TYPE name + +《マッチ度[72.727273]》複合型を作成するためには、すべての属性型に対してUSAGE権限を持たなければなりません。 +《機械翻訳》«To be able to create a range type, you must have USAGE privilege on the subtype.» diff --git a/doc/src/sgml/ref/drop_subscription.sgml b/doc/src/sgml/ref/drop_subscription.sgml index b47d14e4d4a..92757caba43 100644 --- a/doc/src/sgml/ref/drop_subscription.sgml +++ b/doc/src/sgml/ref/drop_subscription.sgml @@ -122,6 +122,7 @@ DROP SUBSCRIPTION [ IF EXISTS ] name注釈 + +《機械翻訳》«When dropping a subscription that is associated with a replication slot on the remote host (the normal state), DROP SUBSCRIPTION will connect to the remote host and try to drop the replication slot (and any remaining table synchronization slots) as part of its operation. This is necessary so that the resources allocated for the subscription on the remote host are released. If this fails, either because the remote host is not reachable or because the remote replication slot cannot be dropped or does not exist or never existed, the DROP SUBSCRIPTION command will fail. To proceed in this situation, first disable the subscription by executing ALTER SUBSCRIPTION ... DISABLE, and then disassociate it from the replication slot by executing» ALTER SUBSCRIPTION ... SET (slot_name = NONE). After that, DROP SUBSCRIPTION will not attempt to drop diff --git a/doc/src/sgml/ref/pg_recvlogical.sgml b/doc/src/sgml/ref/pg_recvlogical.sgml index f1dad63b1c3..daf7910d3f8 100644 --- a/doc/src/sgml/ref/pg_recvlogical.sgml +++ b/doc/src/sgml/ref/pg_recvlogical.sgml @@ -702,9 +702,13 @@ LSNがlsnと正確に一致するレコードがあ 注釈 + +《マッチ度[85.906040]》pg_recvlogicalは、ソースクラスタでグループパーミッションが有効である場合、受け取ったWALファイルのグループパーミッションを維持します。 +《機械翻訳》«pg_recvlogical will preserve group permissions on the output files if group permissions are enabled on the source cluster.» diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 1905f0ea475..09e12fc8319 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -3944,6 +3944,7 @@ SELECT \l[x+]または\list[x+] [ pattern ] + +《マッチ度[68.267831]》サーバ内のデータベースについて、その名前、所有者、文字集合符号化方式、およびアクセス権限を一覧表示します。 +patternを指定すると、パターンにマッチする名前を持つデータベースのみを表示します。 +コマンド名にxが付与された場合は、拡張モードで結果が表示されます。 +コマンド名に+が付与された場合は、データベースのサイズ、デフォルトのテーブル空間、および説明も表示します。 +(サイズ情報は現在のユーザが接続可能なデータベースでのみ表示されます。) +《機械翻訳》«List the databases in the server and show their names, owners, character set encodings, and access privileges. If pattern is specified, only databases whose names match the pattern are listed. If x is appended to the command name, the results are displayed in expanded mode. If + is appended to the command name, database sizes, default tablespaces, and descriptions are also displayed. Size information is available for databases on which the current user has CONNECT privilege, or if the current user is a superuser or has privileges of the pg_read_all_stats role.» @@ -5423,9 +5431,13 @@ SELECT 1 \bind \sendpipeline このコマンドは、主にpg_dumppg_dumpall、およびpg_restoreで生成されるプレーンテキストダンプでの使用を目的としていますが、他の場所でも役に立つかもしれません。 + +《マッチ度[70.813397]》他のほとんどのメタコマンドと異なり、行の残り部分はすべて\efの引数であると常に解釈され、引数内の変数の置換も逆引用符の展開も行われません。 +《機械翻訳》«Unlike most other meta-commands, the entire remainder of the line is always taken to be the argument of \unrestrict, and neither variable interpolation nor backquote expansion are performed.» diff --git a/doc/src/sgml/release-18.sgml b/doc/src/sgml/release-18.sgml index c14e952e796..03bd8a9c2d1 100644 --- a/doc/src/sgml/release-18.sgml +++ b/doc/src/sgml/release-18.sgml @@ -13,14 +13,21 @@ + +このリリースは18.4に対し、様々な不具合を修正したものです。 +18メジャーリリースにおける新機能については、を参照してください。 + +《機械翻訳》«Note: 18.5 was never released, due to a regression discovered post-wrap.» @@ -34,25 +41,38 @@ + +《機械翻訳》«However, the first three security entries below describe configuration adjustments and data cleanups that you may need to make after updating.» + +《機械翻訳》«Also, if you have any GIN indexes, see the changelog entry below about possibly-corrupt reltuples values for their tables.» + +《機械翻訳》«Also, if you use contrib/btree_gist or contrib/ltree, you may need to reindex indexes made with those extensions; see the relevant entries below.» + +《マッチ度[85.416667]》しかしながら、18.2より前のバージョンからアップグレードする場合は、を参照してください。 +《機械翻訳》«Also, if you are upgrading from a version earlier than 18.2, see @@ -81,46 +101,64 @@ Branch: REL_18_STABLE [82fd68801] 2026-08-10 06:38:12 -0700 Branch: REL_17_STABLE [4fcccea97] 2026-08-10 06:38:18 -0700 --> + +《機械翻訳》«Restrict logical decoding output plugins to the set specified by a new server parameter output_plugin_libraries » +(Jacob Champion) § § + +《機械翻訳》«Previously, a replication user could select any loadable library for logical decoding, allowing exploits of various sorts. To allow locking this down without breaking setups that worked before, introduce a whitelist of allowed output plugins.» + +《機械翻訳》«By default, only the output plugins shipped as part of PostgreSQL (pgoutput and test_decoding) are included in output_plugin_libraries. Installations that rely on other output plugins must add them after updating the server, for example» output_plugin_libraries = 'pgoutput, test_decoding, my_trusted_decoder' - Additionally, pg_upgrade --check will fail if the + +《機械翻訳》«Additionally, pg_upgrade --check will fail if the output_plugin_libraries parameter on the new cluster does not permit the plugins of logical replication slots on the old cluster, when migrating from versions 17 and later. Make necessary additions to the new cluster's setting before performing pg_upgrade + +《マッチ度[75.757576]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 +(CVE-2026-6476) +《機械翻訳》«The PostgreSQL Project thanks Vladimir Tokarev and Yu Kunpeng for reporting this problem. (CVE-2026-6471)» @@ -144,13 +182,18 @@ Branch: REL_15_STABLE [953be116c] 2026-08-10 06:38:31 -0700 Branch: REL_14_STABLE [e2c48c81f] 2026-08-10 06:38:37 -0700 --> + +《機械翻訳》«Fix contrib/pgcrypto's PGP encryption to detect unsupported ciphers » +(Daniel Gustafsson) § § + +《機械翻訳》«Previously, if OpenSSL rejected the requested cipher (for example, because it is running in FIPS mode, or the legacy provider hasn't been loaded), pgcrypto failed to notice the failure and simply XOR'd the non-encrypted block with the plaintext, rendering the encryption trivially breakable. This will typically occur with deprecated or non-FIPS cipher algorithms (cipher-algo=blowfish/bf, twofish, cast5, or 3des).» + +《機械翻訳》«By default, pgcrypto will now fail to decrypt any messages that were affected in this way. To allow retrieval of such data, a new option ignore-cipher-failure has been added to pgp_pub_decrypt() and pgp_sym_decrypt(). Setting ignore-cipher-failure=1 will restore their previous behavior, allowing the faulty encryption wrapper to be stripped off:» pgp_sym_decrypt(encrypted_column, any key, 'ignore-cipher-failure=1') + +《機械翻訳》«Once the affected messages are identified and stripped of their wrappers, they can then be re-encrypted with a modern algorithm. It is important however that the behavior of OpenSSL be the same as it was when the faulty messages were created: if the set of unsupported algorithms is not the same, this approach will not work. See the documentation for ignore-cipher-failure + +《マッチ度[74.137931]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 +(CVE-2026-6476) +《機械翻訳》«The PostgreSQL Project thanks Shishir Sharma for reporting this problem. (CVE-2026-14663)» @@ -209,15 +265,20 @@ Branch: REL_15_STABLE [8cdbabea7] 2026-08-10 06:38:31 -0700 Branch: REL_14_STABLE [7215c7e96] 2026-08-10 06:38:36 -0700 --> + +《機械翻訳》«Fix psql to skip in-line data following a scripted COPY ... FROM STDIN command, even if the COPY fails before sending PGRES_COPY_IN » +(Tom Lane) § § + +《機械翻訳》«Previously, if a COPY command failed at startup (for instance, because the target table doesn't exist) psql would not realize that and would proceed to read the following in-line data as SQL commands. In the best case that's wrong and in the worst case it's a SQL-injection hazard. Teach psql to recognize syntactically-valid COPY ... FROM STDIN commands and to skip data on its own authority if the server doesn't respond with PGRES_COPY_IN + +《機械翻訳》«While this fix is unlikely to affect any production SQL scripts, test scripts might intentionally exercise failing COPY ... FROM STDIN commands. Those will need to gain a \. data terminator line after each such command.» + +《マッチ度[75.213675]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 +(CVE-2026-6474) +《機械翻訳》«The PostgreSQL Project thanks Alexander Lakhin for reporting this problem. (CVE-2026-6464)» @@ -257,26 +328,38 @@ Branch: REL_15_STABLE [fc933ce00] 2026-08-10 06:38:30 -0700 Branch: REL_14_STABLE [1f49beef2] 2026-08-10 06:38:36 -0700 --> + +《機械翻訳》«Cross-check the output row type of a portal running EXECUTE or FETCH » +(Robert Haas) § + +《機械翻訳》«EXECUTE and FETCH use two portals: an outer one for the statement itself, and an inner one running the query being executed on its behalf. It was previously possible to make the declared row types of the two portals diverge, leading to server memory disclosure and arbitrary code execution.» + +《マッチ度[70.967742]》PostgreSQLプロジェクトは、本問題を報告してくれたCalif.io(ClaudeおよびAnthropic Researchとの協力)に感謝します。 +(CVE-2026-6479) +《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) and Peter Geoghegan for reporting this problem. (CVE-2026-16239)» @@ -293,23 +376,33 @@ Branch: REL_15_STABLE [8f64cc83f] 2026-07-29 17:15:45 +0200 Branch: REL_14_STABLE [5fb3c6389] 2026-07-29 17:15:45 +0200 --> + +《機械翻訳》«Fix buffer overrun with long time zone abbreviation in to_char() » +(Tom Lane) § + +《機械翻訳》«This can easily crash the server, and exploits leading to arbitrary code execution have been reported.» + +《機械翻訳》«The PostgreSQL Project thanks Hcamael, Amjad Shahzad, Tan Zhen of AntAISecurityLab, Tomer Fichman, Zheng Yu, Amy Burnett (OpenAI Codex Security), Rick de Jager, Heewon Song, Sylvie Mayer, Aleksander Alekseev, and Hillai Ben Sasson for reporting this problem. (CVE-2026-14669)» @@ -325,20 +418,32 @@ Branch: REL_15_STABLE [127a0673f] 2026-08-10 06:38:29 -0700 Branch: REL_14_STABLE [890327639] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Fix buffer overrun in regexp match/split functions » +(Masahiko Sawada) § + +《機械翻訳》«If passed invalidly-encoded data, these functions could write past the end of their conversion buffer.» + +《マッチ度[73.949580]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 +(CVE-2026-6474) +《機械翻訳》«The PostgreSQL Project thanks Francesco Verardi for reporting this problem. (CVE-2026-14664)» @@ -354,22 +459,34 @@ Branch: REL_15_STABLE [3b925133b] 2026-08-10 06:38:31 -0700 Branch: REL_14_STABLE [42d333a78] 2026-08-10 06:38:36 -0700 --> + +《機械翻訳》«Harden the ascii() function against invalid input » +(Michael Paquier) § + +《機械翻訳》«By supplying invalidly-encoded input, this function could be coaxed to read and return a few bytes of data that it shouldn't. In assert-enabled builds, its assertions could be triggered too.» + +《マッチ度[71.559633]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 +(CVE-2026-6474) +《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. (CVE-2026-18024)» @@ -382,25 +499,37 @@ Branch: REL_18_STABLE [08454e8b2] 2026-08-10 06:38:12 -0700 Branch: REL_18_STABLE [8d428c6e6] 2026-08-10 06:38:12 -0700 --> + +《機械翻訳》«Fix multirange type handling in pg_restore_attribute_stats() » +(OpenAI Security Research Team) § § + +《機械翻訳》«pg_restore_attribute_stats() treated multirange types just like their underlying range type. This works correctly for the bounds histogram, but it was wrong for all the other statistics kinds.» + +《マッチ度[68.613139]》PostgreSQLプロジェクトは、本問題を報告してくれたPavel Kohoutに感謝します。 +(CVE-2026-6638) +《機械翻訳》«The PostgreSQL Project thanks Amy Burnett (OpenAI Codex Security) for reporting this problem. (CVE-2026-16238)» @@ -416,22 +545,34 @@ Branch: REL_15_STABLE [005ffaa7f] 2026-08-10 06:38:29 -0700 Branch: REL_14_STABLE [59205c7e9] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Make scalarineqsel() check that a constant it expects to be of type tid actually is » +(Tom Lane) § + +《機械翻訳》«This expectation will hold for all the built-in operators that use this estimator, but a maliciously-constructed operator could violate it, leading to a crash or server memory disclosure.» + +《マッチ度[72.477064]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 +(CVE-2026-6476) +《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. (CVE-2026-14668)» @@ -454,22 +595,34 @@ Branch: REL_15_STABLE [4f8b37b6b] 2026-08-10 06:38:29 -0700 Branch: REL_14_STABLE [1e3014f37] 2026-08-10 06:38:34 -0700 --> + +《機械翻訳》«Harden tsvector and tsquery code against overly long values (both individual lexemes and total vector/query length) » +(Tom Lane) § § + +《機械翻訳》«The documented limits were not enforced in all code paths.» + +《マッチ度[65.333333]》PostgreSQLプロジェクトは、本問題を報告してくれたYu KunpengとMartin Heistermannに感謝します。 +(CVE-2026-6477) +《機械翻訳》«The PostgreSQL Project thanks Yuhang Wu, Zhenpeng Lin, Zheng Yu, and Hcamael for reporting these problems. (CVE-2026-14662)» @@ -492,24 +645,36 @@ Branch: REL_15_STABLE [eb2fa2704] 2026-08-10 06:38:29 -0700 Branch: REL_14_STABLE [c7f462838] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Fix various places that mistakenly assumed they would not have to deal with more than FUNC_MAX_ARGS function arguments » +(Tom Lane) § § + +《機械翻訳》«Notably, the server's actual limit on the number of arguments to an aggregate function is FUNC_MAX_ARGS - 1, but the parser failed to enforce that, creating hazards downstream.» + +《マッチ度[65.972222]》PostgreSQLプロジェクトは、本問題を報告してくれたYu KunpengとMartin Heistermannに感謝します。 +(CVE-2026-6477) +《機械翻訳》«The PostgreSQL Project thanks Zheng Yu, ylwangtju, and Masahiko Sawada for reporting these problems. (CVE-2026-14679)» @@ -532,22 +697,34 @@ Branch: REL_15_STABLE [d6e861e19] 2026-08-10 06:38:29 -0700 Branch: REL_14_STABLE [913fe0c31] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Reject calls from SQL to functions that take or return type internal » +(Tom Lane) § § + +《機械翻訳》«The existing defenses against doing this have been shown to be insufficient, so add more explicit checks.» + +《マッチ度[67.883212]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 +(CVE-2026-6473) +《機械翻訳》«The PostgreSQL Project thanks Amy Burnett (OpenAI Codex Security) for reporting this problem. (CVE-2026-14680)» @@ -563,21 +740,33 @@ Branch: REL_15_STABLE [fb6d1ca8d] 2026-08-10 06:38:29 -0700 Branch: REL_14_STABLE [70a3b4e18] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Preserve the ownership of extended statistics objects when they are rebuilt by ALTER TABLE » +(Masahiko Sawada) § + +《機械翻訳》«Previously, the role running ALTER TABLE gained ownership of such objects, but that seems inappropriate.» + +《マッチ度[75.675676]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 +(CVE-2026-6473) +《機械翻訳》«The PostgreSQL Project thanks Noah Misch for reporting this problem. (CVE-2026-6469)» @@ -593,24 +782,36 @@ Branch: REL_15_STABLE [44ea6764b] 2026-08-10 06:38:30 -0700 Branch: REL_14_STABLE [967acab87] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«When deparsing an EXTRACT() function call, quote the field name if needed » +(Nathan Bossart) § + +《機械翻訳》«The parser accepts any string literal as a field name in EXTRACT(), deferring validation to execution. If the call is stored and deparsed (for example during pg_dump), the string body was regurgitated verbatim, allowing SQL injection.» + +《マッチ度[78.915663]》PostgreSQLプロジェクトは、本問題を報告してくれたCalif.io(ClaudeおよびAnthropic Researchとの協力)に感謝します。 +(CVE-2026-6479) +《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) for reporting this problem. (CVE-2026-15741)» @@ -640,27 +841,39 @@ Branch: REL_15_STABLE [6dbedd48b] 2026-08-10 06:38:30 -0700 Branch: REL_14_STABLE [1a358b8f2] 2026-08-10 06:38:36 -0700 --> + +《機械翻訳》«Check for USAGE privilege on data types in places that formerly failed to check that » +(Nathan Bossart) § § § + +《機械翻訳》«CREATE TYPE AS RANGE did not check, nor did ALTER TABLE OF, nor did commands that create stored expressions. These omissions allowed roles without USAGE privilege to nonetheless create objects depending on the type, possibly blocking the type's owner from changing the type later.» + +《マッチ度[75.892857]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 +(CVE-2026-6476) +《機械翻訳》«The PostgreSQL Project thanks Jingzhou Fu for reporting this problem. (CVE-2026-6470)» @@ -676,23 +889,35 @@ Branch: REL_15_STABLE [17b6083db] 2026-08-10 06:38:30 -0700 Branch: REL_14_STABLE [f4174aa84] 2026-08-10 06:38:36 -0700 --> + +《機械翻訳》«Invalidate role-dependent cached plans after role changes » +(Ilya Staroverov, Shinya Kato, Nathan Bossart) § + +《機械翻訳》«Role membership, role attribute, and database ownership changes may impact the expected behavior of row-level security policies, but previously we'd continue to use cached plans that were made according to the old state of affairs.» + +《マッチ度[69.172932]》PostgreSQLプロジェクトは、本問題を報告してくれたPavel Kohoutに感謝します。 +(CVE-2026-6638) +《機械翻訳》«The PostgreSQL Project thanks Ilya Staroverov and Shinya Kato for reporting this problem. (CVE-2026-14666)» @@ -705,24 +930,36 @@ Branch: REL_18_STABLE [203a48209] 2026-08-10 06:38:11 -0700 Branch: REL_17_STABLE [067a64d40] 2026-08-10 06:38:18 -0700 --> + +《機械翻訳》«Reject GSSEncRequest after direct SSL connection » +(Michael Paquier) § + +《機械翻訳》«After establishing a TLS-encrypted connection, the server would still accept a request for GSSAPI encryption. If that succeeded, the connection would proceed using TLS encryption, but it would look like a GSS connection to the pg_hba rules. Thus, a pg_hba policy intending to disallow TLS would not be enforced correctly.» + +《マッチ度[71.028037]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 +(CVE-2026-6476) +《機械翻訳》«The PostgreSQL Project thanks p4p3r for reporting this problem. (CVE-2026-14681)» @@ -736,11 +973,16 @@ Branch: REL_17_STABLE [dec60e8ad] 2026-08-10 06:38:18 -0700 Branch: REL_16_STABLE [fadbe882d] 2026-08-10 06:38:24 -0700 --> + +《機械翻訳》«Make mock SCRAM authentication secrets more plausible » +(Nathan Bossart) § + +《機械翻訳》«If a SCRAM login is attempted against a role that doesn't exist or doesn't have a SCRAM secret, we generate a mock secret and carry out the authentication handshake anyway, to avoid revealing these facts to an attacker. But the mock secret was made with a fixed iteration count, which in itself can be an observable response discrepancy. Use the configuration setting scram_iterations instead, to make the mock secret look more like the installation's real secrets.» + +《マッチ度[75.221239]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 +(CVE-2026-6473) +《機械翻訳》«The PostgreSQL Project thanks Radim Marek for reporting this problem. (CVE-2026-14672)» @@ -771,24 +1020,36 @@ Branch: REL_15_STABLE [5737110b6] 2026-08-10 06:38:30 -0700 Branch: REL_14_STABLE [74c59d062] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Fix out-of-bounds writes in ecpg applications caused by invalid bytea data received from the server » +(Michael Paquier) § + +《機械翻訳》«ecpg assumed without checking that any bytea value must begin with \x. A broken or malicious server might send a string shorter than 2 bytes, resulting in memory clobber in the application.» + +《マッチ度[72.972973]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 +(CVE-2026-6473) +《機械翻訳》«The PostgreSQL Project thanks ylwangtju for reporting this problem. (CVE-2026-16241)» @@ -804,25 +1065,37 @@ Branch: REL_15_STABLE [df245c374] 2026-08-10 06:38:31 -0700 Branch: REL_14_STABLE [2006fca40] 2026-08-10 06:38:37 -0700 --> + +《機械翻訳》«Do not do backquote expansion on the argument of psql's \unrestrict command » +(Nathan Bossart) § + +《機械翻訳》«This oversight in the fix for CVE-2025-8714 allows a malicious server to inject shell commands into plain-text dump output that will be run at restore time on the machine running psql, the exact scenario that CVE-2025-8714 intended to prevent.» + +《マッチ度[65.068493]》PostgreSQLプロジェクトは、本問題を報告してくれたAltan Birlerに感謝します。 +(CVE-2026-2003) +《機械翻訳》«The PostgreSQL Project thanks Lucas Velgus, Filip Janus, and Daniel Bakker for reporting this problem. (CVE-2026-18408)» @@ -838,14 +1111,19 @@ Branch: REL_15_STABLE [71ba5705d] 2026-08-10 06:38:29 -0700 Branch: REL_14_STABLE [57aa21f69] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Remove pg_dump's assumption that pg_proc.protrftypes cannot have more than FUNC_MAX_ARGS entries » +(Tom Lane) § + +《機械翻訳》«Since there could be entries for both input and output arguments, it's feasible for this array's length to exceed FUNC_MAX_ARGS (which constrains only input arguments). Even if that were not so, pg_dump cannot assume that the server was built with the same value of FUNC_MAX_ARGS that it has. An overrun would lead to a memory clobber inside pg_dump + +《マッチ度[74.358974]》PostgreSQLプロジェクトは、本問題を報告してくれたJoe Conwayに感謝します。 +(CVE-2026-6478) +《機械翻訳》«The PostgreSQL Project thanks Masahiko Sawada for reporting this problem. (CVE-2026-19385)» @@ -876,22 +1161,34 @@ Branch: REL_15_STABLE [cf4ae7c3b] 2026-08-10 06:38:29 -0700 Branch: REL_14_STABLE [d7fcfead3] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Harden PL/Perl against tied Perl arrays and hashes » +(Tom Lane) § + +《機械翻訳》«A tied object that doesn't behave like a regular one could lead to memory overwrite, or to constructing a corrupt result array (which would likely cause problems later).» + +《マッチ度[72.477064]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 +(CVE-2026-6476) +《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. (CVE-2026-14670)» @@ -907,22 +1204,34 @@ Branch: REL_15_STABLE [1eb0d5380] 2026-08-10 06:38:30 -0700 Branch: REL_14_STABLE [aff9dac1c] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Fix integer overflows in memory-allocation calculations in PL/Perl and PL/Tcl » +(Heikki Linnakangas) § + +《機械翻訳》«This is the same type of problem as CVE-2026-6473, just in a different part of the code, and is fixed in the same way.» + +《マッチ度[63.313609]》PostgreSQLプロジェクトは、本問題を報告してくれたPositive TechnologiesのAleksey Solovevに感謝します。 +(CVE-2025-12818) +《機械翻訳》«The PostgreSQL Project thanks the Tulya Project (Team Dhiutsa, Bitecope Technologies Private Ltd) for reporting this problem. (CVE-2026-14677)» @@ -938,13 +1247,18 @@ Branch: REL_15_STABLE [e43e74756] 2026-08-10 06:38:30 -0700 Branch: REL_14_STABLE [39d792040] 2026-08-10 06:38:36 -0700 --> + +《機械翻訳》«Ensure that contrib/amcheck functions restrict search_path before executing index expressions » +(Noah Misch) § + +《機械翻訳》«Because amcheck will run such index expressions as the owner of their tables, a caller could potentially hijack search_path-dependent functions to run arbitrary code as the table owner. By default this is not a vulnerability because only superusers are allowed to call amcheck functions; but if that privilege was granted out, it created a larger hazard than the documentation suggests.» + +《マッチ度[70.992366]》PostgreSQLプロジェクトは、本問題を報告してくれたJoe Conwayに感謝します。 +(CVE-2026-6478) +《機械翻訳》«The PostgreSQL Project thanks Yuelin Wang and Jacob Brazeal for reporting this problem. (CVE-2026-14673)» @@ -974,24 +1295,36 @@ Branch: REL_15_STABLE [74916136f] 2026-08-10 06:38:30 -0700 Branch: REL_14_STABLE [9505175f2] 2026-08-10 06:38:36 -0700 --> + +《機械翻訳》«Fix integer overflows in contrib/fuzzystrmatch's levenshtein() and levenshtein_less_equal() functions » +(Nathan Bossart) § + +《機械翻訳》«Passing large cost values to these functions could cause integer overflows, thereby producing nonsensical results, and even causing out-of-bounds writes in some cases.» + +《マッチ度[78.915663]》PostgreSQLプロジェクトは、本問題を報告してくれたCalif.io(ClaudeおよびAnthropic Researchとの協力)に感謝します。 +(CVE-2026-6479) +《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) for reporting this problem. (CVE-2026-15742)» @@ -1003,21 +1336,33 @@ Branch: REL_19_STABLE [bb02eba53] 2026-08-10 06:38:04 -0700 Branch: REL_18_STABLE [8a31ffc2d] 2026-08-10 06:38:11 -0700 --> + +《機械翻訳》«Fix buffer overrun in contrib/pg_stat_statements » +(Álvaro Herrera) § + +《機械翻訳》«Query normalization didn't accurately account for the amount of space the normalized string would require.» + +《マッチ度[60.240964]》PostgreSQLプロジェクトは、本問題を報告してくれたYu KunpengとMartin Heistermannに感謝します。 +(CVE-2026-6477) +《機械翻訳》«The PostgreSQL Project thanks Sajeeb Lohani (with TrendAI Zero Day Initiative) and Yuelin Wang for reporting this problem. (CVE-2026-14676)» @@ -1033,22 +1378,34 @@ Branch: REL_15_STABLE [c7c82a88c] 2026-08-10 06:38:29 -0700 Branch: REL_14_STABLE [a74aa0854] 2026-08-10 06:38:35 -0700 --> + +《機械翻訳》«Fix datatype error in contrib/pg_trgm's GiST picksplit function » +(Heikki Linnakangas) § + +《機械翻訳》«This mistake resulted in reading past the end of the buffer, typically causing bad split decisions; but a crash could ensue if you're very unlucky.» + +《マッチ度[75.862069]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 +(CVE-2026-6473) +《機械翻訳》«The PostgreSQL Project thanks Mehmet D. Ince for reporting this problem. (CVE-2026-14678)» @@ -1063,24 +1420,36 @@ Branch: REL_15_STABLE [b7b513d9a] 2026-06-05 12:08:05 -0500 Branch: REL_14_STABLE [5b72d0279] 2026-06-05 12:08:05 -0500 --> + +《機械翻訳》«Remove the plan cache in contrib/refint » +(Ayush Tiwari) § + +《機械翻訳》«This caching behavior has several serious bugs, notably that check_foreign_key() embeds the new key values in its cascade-UPDATE queries, so a cached plan reuses the originally-needed values rather than the key values that should be used. The simplest solution is to remove it.» + +《マッチ度[72.477064]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 +(CVE-2026-6476) +《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. (CVE-2026-14671)» @@ -1092,13 +1461,18 @@ Branch: REL_19_STABLE [5707d7517] 2026-07-30 14:59:30 +0200 Branch: REL_18_STABLE [d4420a972] 2026-07-30 14:59:39 +0200 --> + +《機械翻訳》«Ensure that parallel GIN index builds update the table's pg_class.reltuples value correctly » +(Jan Nidzwetzki, Tomas Vondra) § + +《機械翻訳》«A parallel worker could report an uninitialized value for the number of rows it processed, leading to a bogus value for reltuples, even Infinity or NaN. Such values could lead to subsequent autovacuum and autoanalyze operations never deciding that the table needs to be processed. If so, the situation will not self-heal. A manual ANALYZE command, or creation of another index, will be needed to reset reltuples to the correct value. If you have any tables with GIN indexes, it's recommended to check to see if their reltuples entries look sane. A query such as this may be helpful:» SELECT DISTINCT t.oid::regclass, t.reltuples FROM pg_class t @@ -1133,13 +1509,18 @@ Branch: REL_15_STABLE [7213cbfa0] 2026-08-07 17:30:05 +0900 Branch: REL_14_STABLE [cec48686a] 2026-08-07 17:30:06 +0900 --> + +《機械翻訳》«Fix mis-handling of asynchronous reads when rescanning an asynchronous Append plan node » +(Alexander Korotkov, Gleb Kashkin, Etsuro Fujita) § + +《機械翻訳》«When an upper plan node rescans an Append before having read the entire Append output, we need to discard any in-flight requests sent to external servers (by postgres_fdw for example). This was not done correctly in cases where a subplan has parameter changes or is discarded by partition pruning in the next scan. The outcome could be incorrect query results, an infinite loop, or an assertion failure.» @@ -1162,14 +1545,21 @@ Branch: REL_15_STABLE [5190732c9] 2026-07-31 15:39:01 +1200 Branch: REL_14_STABLE [6098f35f4] 2026-07-31 15:39:21 +1200 --> + +《機械翻訳》«Fix error in partition pruning for RANGE-partitioned tables » +(David Rowley) § + +《機械翻訳》«In some cases the DEFAULT partition would be skipped when it should not be, which could lead to rows missing from query results.» @@ -1181,17 +1571,24 @@ Branch: REL_18_STABLE [1ef917e3a] 2026-06-23 21:08:50 +0900 Branch: REL_18_STABLE [bba4e095d] 2026-06-25 12:15:02 +0900 --> + +《機械翻訳》«Correctly update foreign-data-wrapper state in a ModifyTable plan node after pruning result relations » +(Ayush Tiwari, Rafia Sabih) § § + +《機械翻訳》«Previously, if run-time partition pruning determined that some partitions of a partitioned target table need not be scanned and the table had any foreign-table partitions, a crash or erroneous behavior was likely.» @@ -1203,19 +1600,26 @@ Branch: REL_19_STABLE [7048e50f8] 2026-07-08 20:46:25 +0100 Branch: REL_18_STABLE [4908225be] 2026-07-08 20:46:26 +0100 --> + +《機械翻訳》«Fix missed concurrent update in UPDATE with RETURNING OLD on a table that has a BEFORE UPDATE trigger » +(Dean Rasheed) § + +《機械翻訳》«If the target row was concurrently updated, then at isolation level READ COMMITTED any OLD values in RETURNING should reflect the updated row. But stale values were returned if there was a trigger (although the trigger itself, and the final output row, saw the correct values).» @@ -1227,16 +1631,23 @@ Branch: REL_19_STABLE [a71a348ed] 2026-07-31 23:24:23 +1200 Branch: REL_18_STABLE [f70acc8a2] 2026-07-31 23:24:46 +1200 --> + +《機械翻訳》«Fix hash join performance issue when there are multiple join keys and many NULL values » +(David Rowley) § + +《機械翻訳》«Null-keyed tuples should not get inserted into the hash table, since they will never match any other tuples. The code got this wrong if the null was in a non-last join column, bloating the hash table quite a lot if many inputs contain nulls.» @@ -1247,16 +1658,23 @@ Branch: master Release: REL_19_BR [79c65b9d9] 2026-06-11 12:08:47 +0100 Branch: REL_18_STABLE [9108fed3e] 2026-06-11 12:08:48 +0100 --> + +《機械翻訳》«Fix parsing of parenthesized OLD/NEW in RETURNING expressions » +(Marko Grujic) § + +《機械翻訳》«Expressions such as (old).colname and (old).* were mis-handled, effectively converting them to NEW references.» @@ -1272,18 +1690,25 @@ Branch: REL_15_STABLE [53470ffba] 2026-07-28 16:09:04 -0400 Branch: REL_14_STABLE [13b627a3e] 2026-07-28 16:09:04 -0400 --> + +《機械翻訳》«Fix planner's nullability and strictness checks for value IN (array) expressions » +(Ayush Tiwari) § + +《機械翻訳》«These checks should only succeed if the array operand is known to be non-empty, but that consideration was missed, allowing optimizations to be applied that should not be. This could result in wrong query answers if the array actually was empty.» @@ -1303,15 +1728,22 @@ Branch: REL_17_STABLE [b308eb366] 2026-07-20 12:16:58 +0900 Branch: REL_16_STABLE [d610d8e8b] 2026-07-20 12:17:40 +0900 --> + +《機械翻訳》«Fix incorrect join removal logic » +(Matheus Alcantara, Richard Guo) § § + +《機械翻訳》«In edge cases, it was possible for a constant output value coming from within the nullable side of an outer join to not be replaced by NULL when it should be.» @@ -1325,15 +1757,22 @@ Branch: REL_19_STABLE [aae47813a] 2026-07-15 09:21:44 +0900 Branch: REL_18_STABLE [18105e6db] 2026-07-15 09:22:58 +0900 --> + +《機械翻訳》«Clean up PlaceHolderVars more thoroughly during join removal » +(Richard Guo, Arne Roland) § § + +《機械翻訳》«This fix corrects various edge cases that could trip assertions or result in incorrect plans.» @@ -1348,17 +1787,24 @@ Branch: REL_15_STABLE [caebac5f1] 2026-06-08 11:48:18 -0400 Branch: REL_14_STABLE [64778fac7] 2026-06-08 11:48:18 -0400 --> + +《機械翻訳》«Add missed checks for hashability of equality comparisons on container datatypes (arrays, composites, ranges) » +(Andrei Lepikhov, Tom Lane) § + +《機械翻訳》«The planner must verify hashability of the container's component type(s) before deciding it can use a hash-based plan type. This step was missed in some places, leading to could not identify a hash function failures at execution.» @@ -1370,16 +1816,23 @@ Branch: REL_19_STABLE [98d5d7ee6] 2026-07-06 16:14:13 +0900 Branch: REL_18_STABLE [fe5d62951] 2026-07-06 16:15:45 +0900 --> + +《機械翻訳》«Avoid pushing WHERE clauses down past a grouping step that has a different equivalence rule » +(Richard Guo) § + +《機械翻訳》«A test on a grouping column that is grouped by a nondeterministic collation is safe to push down only if it is a comparison using that same collation. Otherwise it might filter some rows the grouping would have merged.» @@ -1394,15 +1847,22 @@ Branch: REL_16_STABLE [a85732162] 2026-07-08 00:00:03 +1200 Branch: REL_15_STABLE [842e34efa] 2026-07-08 00:00:34 +1200 --> + +《機械翻訳》«Fix mis-optimization of COUNT window functions that have an EXCLUDE clause or lack ORDER BY » +(Chengpeng Yan, David Rowley) § + +《機械翻訳》«These window functions were treated as monotonic when they should not be, allowing wrong answers to be computed.» @@ -1413,9 +1873,13 @@ Branch: master Release: REL_19_BR [b574fec00] 2026-06-28 12:31:29 -0400 Branch: REL_18_STABLE [5fd1c3f28] 2026-06-28 12:31:29 -0400 --> + +《機械翻訳》«Avoid cache lookup failed for collation 0 error when planner looks up statistics for a column of type "char" » +(Feng Wu) § @@ -1432,8 +1896,12 @@ Branch: REL_15_STABLE [785289de0] 2026-08-04 09:06:46 +0200 Branch: REL_14_STABLE [cad17745e] 2026-08-04 09:06:46 +0200 --> + +《機械翻訳》«Fix ALTER COLUMN ... DROP EXPRESSION to work when there are multiple levels of partitions » +(Alberto Piai) § @@ -1447,14 +1915,21 @@ Branch: REL_18_STABLE [19e3aa704] 2026-07-20 17:21:20 +0200 Branch: REL_17_STABLE [1d6c654c8] 2026-07-20 17:21:20 +0200 --> + +《機械翻訳》«Fix attaching partitions of indexes that are exclusion constraints » +(Japin Li) § + +《機械翻訳》«Notably, this oversight broke dump/restore of partitioned exclusion constraints.» @@ -1465,18 +1940,25 @@ Branch: master Release: REL_19_BR [d8b5d87e5] 2026-05-22 23:59:04 +0900 Branch: REL_18_STABLE [41247cdf6] 2026-05-23 00:01:24 +0900 --> + +《機械翻訳》«Prevent setting NO INHERIT on partitioned NOT NULL constraints via ALTER CONSTRAINT » +(Andreas Karlsson) § + +《機械翻訳》«NOT NULL constraints on partitioned tables are supposed to be inherited by all partitions, and therefore must not be marked NO INHERIT. This rule was correctly enforced by constraint creation, but not by ALTER TABLE ... ALTER CONSTRAINT @@ -1492,14 +1974,21 @@ Branch: REL_15_STABLE [eada45dd8] 2026-07-04 11:34:26 -0400 Branch: REL_14_STABLE [1b17a6e3c] 2026-07-04 11:34:26 -0400 --> + +《機械翻訳》«Disallow renaming a rule to _RETURN » +(Tom Lane) § + +《機械翻訳》«That name is reserved for a view's ON SELECT rule, but ALTER RULE allowed renaming other rules to _RETURN, causing trouble later.» @@ -1513,14 +2002,21 @@ Branch: REL_17_STABLE [25e54cec7] 2026-08-03 12:25:41 -0700 Branch: REL_16_STABLE [e61d44fde] 2026-08-03 12:25:49 -0700 --> + +《機械翻訳》«Fix missing lock release for role membership grants in DROP OWNED BY » +(Jeff Davis) § + +《機械翻訳》«This oversight resulted in a warning message, followed by retaining a lock on the membership grant until the end of the transaction.» @@ -1534,14 +2030,21 @@ Branch: REL_17_STABLE [dcda1f07d] 2026-07-08 08:50:14 +0900 Branch: REL_16_STABLE [485527190] 2026-07-08 08:51:09 +0900 --> + +《機械翻訳》«Fix failure of EXPLAIN when deparsing SQL/JSON aggregates » +(Richard Guo) § + +《機械翻訳》«Some plan structures resulted in invalid JsonConstructorExpr underlying node type errors.» @@ -1557,15 +2060,22 @@ Branch: REL_15_STABLE [5b3712e31] 2026-07-28 08:35:19 +0900 Branch: REL_14_STABLE [55adef7ab] 2026-07-28 08:35:21 +0900 --> + +《機械翻訳》«Fix use of REINDEX CONCURRENTLY with a deferred uniqueness constraint » +(Nitin Motiani) § + +《機械翻訳》«The transient index copy created during REINDEX CONCURRENTLY was incorrectly marked as enforcing immediate uniqueness, causing spurious reports of constraint violation.» @@ -1580,13 +2090,18 @@ Branch: REL_19_STABLE [54d5947ef] 2026-07-06 14:47:58 -0400 Branch: REL_18_STABLE [51652c42d] 2026-07-06 14:47:58 -0400 --> + +《機械翻訳》«Fix LIKE matching with nondeterministic collations and backslashes » +(Nitin Motiani, Tom Lane) § § + +《機械翻訳》«When using a nondeterministic collation, LIKE mishandled an escaped backslash (\\), treating it as effectively not there. It also did the wrong thing with a leading backslash preceding an ordinary character; in that case the backslash should be effectively ignored, but it caused the ordinary character to be matched exactly rather than allowing the nondeterministic collation to decide if there's a match.» @@ -1605,12 +2122,17 @@ Branch: REL_19_STABLE [67cf73ddb] 2026-07-06 13:06:25 -0400 Branch: REL_18_STABLE [d0bb49e61] 2026-07-06 13:06:25 -0400 --> + +《機械翻訳》«Fix LIKE/regex optimization for indexscan with exact-match pattern » +(Jelte Fennema-Nio) § + +《機械翻訳》«Refactoring for LIKE with non-deterministic collations accidentally broke the optimization for converting a LIKE or regex exact-match pattern to an equality index condition when the index collation doesn't match the expression collation. Among other things, that made psql's \d tablename command much slower.» @@ -1636,15 +2160,22 @@ Author: Heikki Linnakangas Branch: REL_18_STABLE [5f003855e] 2026-08-11 21:24:28 +0300 --> + +《機械翻訳》«Fix matching of localized month/day names in to_date() » +(Heikki Linnakangas) § § + +《機械翻訳》«The matching logic misbehaved in cases where case-folding changes the byte length of the string.» @@ -1656,14 +2187,21 @@ Branch: REL_19_STABLE [28d498e28] 2026-07-07 14:29:21 -0700 Branch: REL_18_STABLE [66ec24276] 2026-07-07 15:04:31 -0700 --> + +《機械翻訳》«Correct case-folding rules for Greek final sigma » +(Jeff Davis) § + +《機械翻訳》«If the string is preceded only by Case Ignorable characters, don't consider it to be a final sigma. This only affects the built-in pg_unicode_fast locale.» @@ -1678,14 +2216,21 @@ Branch: REL_15_STABLE [c391375ba] 2026-06-05 07:50:16 +0900 Branch: REL_14_STABLE [8bb935d61] 2026-06-05 07:50:18 +0900 --> + +《機械翻訳》«Fix incorrect NFC recomposition for Hangul U+11A7 (TBASE) » +(Diego Frias, Michael Paquier) § + +《機械翻訳》«This character was treated as a valid T syllable, which it is not, and hence silently swallowed during normalization.» @@ -1700,14 +2245,21 @@ Branch: REL_15_STABLE [a7e0e42a2] 2026-06-08 11:49:11 -0700 Branch: REL_14_STABLE [1e0458172] 2026-06-08 11:49:27 -0700 --> + +《機械翻訳》«Avoid possible truncation of output lexemes in case-insensitive synonym dictionaries » +(Jeff Davis) § + +《機械翻訳》«If folding to lower case increased the byte length of a lexeme, it was incorrectly truncated to its original byte length when emitted.» @@ -1720,8 +2272,12 @@ Branch: REL_18_STABLE [9021c8f3c] 2026-07-07 13:35:15 -0700 Branch: REL_17_STABLE [5e78ebca5] 2026-07-07 13:35:23 -0700 --> + +《機械翻訳》«Defend against truncated UTF-8 characters in case-conversion logic » +(Jeff Davis) § @@ -1737,16 +2293,23 @@ Branch: REL_15_STABLE [259b627d5] 2026-06-03 12:47:32 +0900 Branch: REL_14_STABLE [74d3482f4] 2026-06-03 12:47:34 +0900 --> + +《機械翻訳》«Fix typo in hash_record_extended() » +(Man Zeng) § + +《機械翻訳》«The code failed to initialize the second isnull argument passed to FunctionCallInvoke(). This is harmless for existing in-core extended hash support functions, which will not examine that value. However, extension-provided hash functions could be affected if they inspect PG_ARGISNULL(1) @@ -1760,8 +2323,12 @@ Branch: REL_17_STABLE [dcbc96685] 2026-07-28 10:39:47 -0700 Branch: REL_16_STABLE [6c760f6b6] 2026-07-28 10:39:50 -0700 --> + +《機械翻訳》«Fix pg_get_publication_tables() to not fail if a publishable table is dropped concurrently » +(Bharath Rupireddy) § @@ -1778,8 +2345,12 @@ Branch: REL_15_STABLE [d39b9eed0] 2026-07-06 12:24:23 -0400 Branch: REL_14_STABLE [0115650de] 2026-07-06 12:24:28 -0400 --> + +《機械翻訳》«Prevent satisfies_hash_partition() from crashing with VARIADIC NULL » +(Robert Haas) § @@ -1795,17 +2366,24 @@ Branch: REL_15_STABLE [b3a86eb6d] 2026-06-04 12:24:51 -0400 Branch: REL_14_STABLE [262cc4df2] 2026-06-04 12:24:51 -0400 --> + +《機械翻訳》«Report invalid-weight errors more cleanly and consistently in tsvector_filter() and allied functions » +(Ewan Young) § + +《機械翻訳》«In particular, report weight characters that are not printable ASCII in octal form (\nnn), as charout() would render them. This avoids possibly producing an invalidly-encoded error message.» @@ -1817,15 +2395,22 @@ Branch: REL_19_STABLE [2a933deaa] 2026-07-16 11:50:16 -0700 Branch: REL_18_STABLE [c31b0fca0] 2026-07-16 11:50:13 -0700 --> + +《機械翻訳》«Reject out-of-range timestamp shift values in uuidv7() » +(Baji Shaik) § + +《機械翻訳》«The shift value must not be so large as to produce a timestamp out of the range that a v7 UUID can represent. Previously, a garbage UUID value was produced.» @@ -1846,15 +2431,22 @@ Branch: REL_15_STABLE [9618e790c] 2026-06-12 12:39:34 +0900 Branch: REL_14_STABLE [a17f39aa2] 2026-06-12 12:39:40 +0900 --> + +《機械翻訳》«Fix mishandling of namespace nodes in xpath() » +(Michael Paquier) § § + +《機械翻訳》«This fix avoids an unexpected could not copy node error.» @@ -1869,14 +2461,21 @@ Branch: REL_18_STABLE [90789900b] 2026-07-02 15:06:05 +0900 Branch: REL_17_STABLE [c768637d6] 2026-07-02 15:06:12 +0900 --> + +《機械翻訳》«Fix jsonpath's .decimal method to not throw a hard error for incorrect precision or scale » +(Ewan Young) § § + +《機械翻訳》«Silent mode should suppress these errors, but failed to.» @@ -1889,16 +2488,23 @@ Branch: REL_17_STABLE [d0acd2535] 2026-06-11 16:17:58 +0200 Branch: REL_16_STABLE [60abb3c73] 2026-06-11 16:17:58 +0200 --> + +《機械翻訳》«Fix NULL-pointer crash when IS JSON or similar constructs have an argument that is of string category but lacks a cast to type text » +(Ayush Tiwari) § + +《機械翻訳》«There are no such data types in core PostgreSQL, but the problem is reachable with some extension types.» @@ -1911,15 +2517,22 @@ Branch: REL_18_STABLE [441e4c8d6] 2026-07-07 08:27:05 +0900 Branch: REL_17_STABLE [71cd10cd2] 2026-07-07 08:26:50 +0900 --> + +《機械翻訳》«Ensure that SQL/JSON ON EMPTY / ON ERROR DEFAULT values are coerced to the correct typmod » +(Ewan Young) § + +《機械翻訳》«For example, the declared precision and scale of a numeric target column were not applied to the default value.» @@ -1935,8 +2548,12 @@ Branch: REL_15_STABLE [d782c97e1] 2026-08-04 18:01:31 +1200 Branch: REL_14_STABLE [fec40878c] 2026-08-04 18:01:55 +1200 --> + +《機械翻訳》«Avoid machine-dependent behavior when dividing the smallest possible money value by -1 » +(Andrey Rachitskiy) § @@ -1953,8 +2570,12 @@ Branch: REL_15_STABLE [025228104] 2026-08-02 16:49:18 -0400 Branch: REL_14_STABLE [dda622edc] 2026-08-02 16:49:18 -0400 --> + +《機械翻訳》«Fix crash after out-of-memory failure partway through creation of a cache entry for a text search dictionary » +(Tom Lane) § @@ -1971,8 +2592,12 @@ Branch: REL_15_STABLE [0fb88979b] 2026-08-02 13:22:39 -0400 Branch: REL_14_STABLE [cfc720ef4] 2026-08-02 13:22:39 -0400 --> + +《機械翻訳》«Fix memory-safety bugs in processing of incorrect ispell/hunspell dictionary files » +(Andrey Rachitskiy) § @@ -1990,15 +2615,22 @@ Branch: REL_17_STABLE [4e49f68b7] 2026-07-03 18:01:03 +0300 Branch: REL_16_STABLE [cc3fe7e2a] 2026-07-03 18:01:00 +0300 --> + +《機械翻訳》«Prevent access to other sessions' temporary tables » +(Jim Jones, Daniil Davydov, Alexander Korotkov) § § + +《機械翻訳》«Some code paths failed to prevent this, leading to silently wrong (inconsistent) results.» @@ -2009,16 +2641,23 @@ Branch: master Release: REL_19_BR [da6874635] 2026-04-21 11:03:05 -0400 Branch: REL_18_STABLE [ed8050370] 2026-08-05 11:44:01 -0400 --> + +《機械翻訳》«Prevent no empty local buffer available errors during temporary table access » +(Melanie Plageman) § + +《機械翻訳》«Limit the number of local buffers that the read streaming mechanism is allowed to use. Previously, a large value of effective_io_concurrency could allow a single stream to use all the buffers, resulting in failure.» @@ -2031,14 +2670,21 @@ Branch: REL_18_STABLE [4cc49cb70] 2026-07-31 10:34:40 -0500 Branch: REL_17_STABLE [288d4e83f] 2026-07-31 10:34:40 -0500 --> + +《機械翻訳》«Fix the order in which autovacuum processes databases » +(Rustam Khamidullin) § + +《機械翻訳》«It was unintentionally processing databases from lowest to highest score, when it should be doing the reverse.» @@ -2053,20 +2699,27 @@ Branch: REL_19_STABLE [55d01a10f] 2026-08-07 10:05:23 -0400 Branch: REL_18_STABLE [7c25cdb1e] 2026-08-07 10:06:41 -0400 --> + +《機械翻訳》«Restore full use of shared buffer pool in VACUUM's wraparound failsafe mode » +(Melanie Plageman) § § + +《機械翻訳》«An ordinary VACUUM is limited to use just a few shared buffers, so as not to impinge too much on other processing. However, in failsafe mode we want to reclaim transaction IDs as quickly as possible, so that limit is supposed to be abandoned to allow vacuuming to proceed as fast as possible. This behavior was accidentally broken during refactoring in v18; restore it.» @@ -2078,13 +2731,20 @@ Branch: REL_18_STABLE [4154a1482] 2026-06-08 15:29:19 +0900 Branch: REL_17_STABLE [8ad414831] 2026-06-08 15:29:21 +0900 --> + +《機械翻訳》«Fix memory leak in parallel vacuum worker processes » +(Baji Shaik) § + +《機械翻訳》«Progress reports from a parallel worker leaked about 1kB per report, with the waste accumulating for the life of the worker process.» @@ -2100,15 +2760,22 @@ Branch: REL_15_STABLE [7123abab7] 2026-07-28 10:56:38 +0200 Branch: REL_14_STABLE [15fd7a3e2] 2026-07-28 10:56:39 +0200 --> + +《機械翻訳》«Honor query cancel and vacuum delay during GIN index posting-tree cleanup » +(Paul Kim, Alexander Korotkov) § + +《機械翻訳》«The posting tree for a common value can be large, so that this missed check could allow vacuum to run for a long time before noticing an interrupt.» @@ -2124,16 +2791,23 @@ Branch: REL_15_STABLE [126141425] 2026-07-17 15:52:58 -0400 Branch: REL_14_STABLE [e4ad22eb0] 2026-07-17 15:52:56 -0400 --> + +《機械翻訳》«Fix possible mis-decoding of index tuples during GiST and SP-GiST index-only scans » +(Peter Geoghegan) § + +《機械翻訳》«This error could lead to emitting corrupted data from an index-only scan plan. The only affected core opclass is GiST's range_ops, and it could only fail if the range column were not the first index column.» @@ -2147,16 +2821,23 @@ Branch: REL_17_STABLE [768ae083e] 2026-07-15 15:56:06 -0400 Branch: REL_16_STABLE [0fd5595aa] 2026-07-15 15:58:15 -0400 --> + +《機械翻訳》«Ensure that the new last block of a bulk-extended table is added to its free space map promptly » +(Jingtang Zhang) § + +《機械翻訳》«An off-by-one error caused the last block of a multi-block table extension to not be marked as free in the map. This would eventually get corrected by vacuum, but meanwhile the space wouldn't be used.» @@ -2168,8 +2849,12 @@ Branch: REL_18_STABLE [ac6a58a70] 2026-06-22 18:03:23 -0400 Branch: REL_17_STABLE [011eedcdc] 2026-06-22 18:03:23 -0400 --> + +《機械翻訳》«Avoid possible double-free or infinite error recovery loop in resource cleanup during transaction abort » +(Tom Lane) § @@ -2185,8 +2870,12 @@ Branch: REL_15_STABLE [4647ac142] 2026-06-19 12:52:00 -0400 Branch: REL_14_STABLE [4b3bc6b71] 2026-06-19 12:52:00 -0400 --> + +《機械翻訳》«When creating directories, tolerate concurrent creation of the same directory » +(Andrew Dunstan, Tom Lane) § @@ -2198,8 +2887,12 @@ Branch: master Release: REL_19_BR [dc5116780] 2026-06-19 15:26:18 +1200 Branch: REL_18_STABLE [e9692de1d] 2026-06-19 15:26:51 +1200 --> + +《機械翻訳》«Fix JIT-compiled tuple deconstruction code to account correctly for virtual generated columns » +(David Rowley) § @@ -2221,13 +2914,18 @@ Branch: REL_15_STABLE [ef3d7b15e] 2026-05-27 18:37:27 +0300 Branch: REL_14_STABLE [36b6ed260] 2026-05-27 18:37:48 +0300 --> + +《機械翻訳》«Prevent creation of dangling object dependencies by acquiring a shared lock on any object being depended on » +(Bertrand Drouvot) § § + +《機械翻訳》«The shared lock will conflict with any attempt to drop the depended-on object, eliminating the race condition that formerly existed. For example, if one session drops a schema (that appears empty to it) concurrently with some other session creating a function in that schema, previously both transactions could commit, leaving an invalid function definition behind. Now, one transaction or the other will fail.» @@ -2250,16 +2950,23 @@ Branch: REL_15_STABLE [5d18105ca] 2026-07-25 12:01:26 -0400 Branch: REL_14_STABLE [2fc3e1b44] 2026-07-25 12:01:24 -0400 --> + +《機械翻訳》«Fix race condition in conflict detection for SERIALIZABLE isolation mode » +(Peter Geoghegan) § + +《機械翻訳》«A conflict could be missed when examining an initially-empty btree index, allowing failure of serializability due to improperly allowing conflicting transactions to commit.» @@ -2273,15 +2980,22 @@ Branch: REL_16_STABLE [cdb9b2830] 2026-05-27 16:26:05 -0700 Branch: REL_15_STABLE [159324a73] 2026-05-27 16:26:08 -0700 --> + +《機械翻訳》«Fix race condition in ProcSignalBarrier code » +(Masahiko Sawada) § + +《機械翻訳》«This error could result in processes getting stuck, typically after reporting still waiting for backend with PID nnnn to accept ProcSignalBarrier @@ -2302,17 +3016,24 @@ Branch: REL_15_STABLE [e786fb5aa] 2026-05-27 17:19:58 +0900 Branch: REL_14_STABLE [db4d12fc9] 2026-05-27 17:20:00 +0900 --> + +《機械翻訳》«Fix race conditions when a set of processes that belong to the same lock group exit at the same time » +(Vlad Lesin) § § + +《機械翻訳》«These errors could lead to PANIC aborts, with messages such as latch already owned. The issue does not normally arise in regular parallel query, since the leader won't exit before seeing its workers finish; but some extensions reach the problem.» @@ -2333,19 +3054,26 @@ Branch: REL_18_STABLE [4d7feebfb] 2026-07-15 17:37:34 -0400 Branch: REL_17_STABLE [067213430] 2026-07-15 17:43:38 -0400 --> + +《機械翻訳》«Fix WAL logging of operations that clear bits in tables' visibility maps » +(Melanie Plageman, Andres Freund) § § § + +《機械翻訳》«Such VM changes were missed by the WAL summarizer, potentially leading to incorrect incremental backups. We also failed to log full-page images of such VM pages when needed, potentially allowing torn page writes to go uncorrected. This could lead to misbehavior later, such as wrong results from index-only scans.» @@ -2362,8 +3090,12 @@ Branch: REL_18_STABLE [1d299d6ab] 2026-07-22 08:49:30 -0400 Branch: REL_17_STABLE [d28cdf46e] 2026-07-22 08:49:00 -0400 --> + +《機械翻訳》«Prevent WAL summarizer process from getting stuck at a timeline switch » +(Robert Haas) § § @@ -2383,17 +3115,24 @@ Branch: REL_17_STABLE [ab5334d8b] 2026-06-12 11:44:16 +0900 Branch: REL_16_STABLE [d9b49e5b4] 2026-06-12 11:44:19 +0900 --> + +《機械翻訳》«Fix race with timeline selection in logical decoding during standby promotion » +(Bertrand Drouvot) § § + +《機械翻訳》«Logical decoding being performed on the standby could fail with a requested WAL segment has already been removed error. A repeat attempt would succeed, so there was no permanent problem but there was an availability hazard.» @@ -2408,16 +3147,23 @@ Branch: REL_15_STABLE [065cbfb88] 2026-05-23 08:10:17 +0900 Branch: REL_14_STABLE [e18b77153] 2026-05-23 08:10:18 +0900 --> + +《機械翻訳》«Avoid exposing a WAL receiver's full connection string during timeline jumps » +(Chao Li) § + +《機械翻訳》«The pg_stat_wal_receiver view should show a sanitized version of the connection string, without sensitive data. But it transiently showed the full string when we re-use an existing WAL receiver.» @@ -2432,16 +3178,23 @@ Branch: REL_15_STABLE [871d4f5b6] 2026-05-16 18:01:40 -0700 Branch: REL_14_STABLE [510a05f07] 2026-05-16 18:01:46 -0700 --> + +《機械翻訳》«Use run-time checks, not just Asserts, to verify the correct number of columns in tuples received during logical replication » +(Varik Matevosyan) § + +《機械翻訳》«A malicious or buggy publisher could send inconsistent numbers of columns. While we could not find a scenario in which this would have serious ill effects, extra caution seems warranted.» @@ -2456,12 +3209,17 @@ Branch: REL_15_STABLE [819e5b964] 2026-06-15 15:35:37 -0400 Branch: REL_14_STABLE [2a00840e8] 2026-06-15 15:35:37 -0400 --> + +《機械翻訳》«Clean up quoting of string parameters within constructed replication commands » +(Tom Lane) § + +《機械翻訳》«Various places that generate replication commands were not being adequately careful about quoting replication slot names and other parameters that need to be inserted into those commands. This could result in unexpected syntax errors in those commands. In principle, a crafted replication slot name could result in SQL injection; but such a scenario seems very unlikely to occur in practice, since replication operations can only be invoked by highly-privileged users and there is no reason for them to use a slot name coming from an untrustworthy source.» @@ -2486,16 +3246,23 @@ Branch: REL_15_STABLE [0dbfb8520] 2026-07-28 12:33:47 -0700 Branch: REL_14_STABLE [c2d34db0a] 2026-07-28 12:33:50 -0700 --> + +《機械翻訳》«Fix logical decoding of empty prepared transactions » +(Masahiko Sawada) § + +《機械翻訳》«A prepared transaction that did not cause any decodable updates could result in sending COMMIT/ROLLBACK PREPARED to the output plugin with no preceding PREPARE. For the built-in subscriber this breaks replication, and other plugins will probably not like it either.» @@ -2510,16 +3277,23 @@ Branch: REL_16_STABLE [913d3b610] 2026-06-30 08:51:51 +0900 Branch: REL_15_STABLE [d2980067b] 2026-06-30 08:52:50 +0900 --> + +《機械翻訳》«Fix corruption of unlogged sequences after standby promotion » +(Fujii Masao) § + +《機械翻訳》«Previously, if an unlogged sequence was created on the primary and replicated to a standby, accessing the sequence after promoting the standby could fail with bad magic number in sequence or related errors.» @@ -2535,15 +3309,22 @@ Branch: REL_15_STABLE [8f64cc83f] 2026-07-29 17:15:45 +0200 Branch: REL_14_STABLE [5fb3c6389] 2026-07-29 17:15:45 +0200 --> + +《機械翻訳》«Fix cascading standby reconnect failure after archive fallback » +(Marco Nenciarini) § + +《機械翻訳》«A cascading standby could fail to reconnect to its upstream standby with requested starting point ... is ahead of the WAL flush position after falling back to archive recovery.» @@ -2555,8 +3336,12 @@ Branch: REL_19_STABLE [7673dfe77] 2026-08-08 00:10:00 +0900 Branch: REL_18_STABLE [311e66df9] 2026-08-08 00:10:06 +0900 --> + +《機械翻訳》«Prevent accepting hot-standby connections before WAL replay has reached a consistent database state » +(Nikhil Sontakke) § @@ -2569,17 +3354,24 @@ Branch: REL_18_STABLE [97b5c5aaa] 2026-05-27 02:28:39 +0300 Branch: REL_17_STABLE [4a375527a] 2026-05-27 02:28:49 +0300 --> + +《機械翻訳》«Do not try to clear pg_database.dathasloginevt locally on a standby server » +(Ayush Tiwari) § + +《機械翻訳》«Event trigger cleanup tried to perform that action on standby servers as well as the primary. That can't work on a standby, and there's no need anyway since replay of the primary's database change will soon fix it.» @@ -2591,14 +3383,21 @@ Branch: REL_18_STABLE [08458bcae] 2026-06-18 09:42:56 +0530 Branch: REL_17_STABLE [ea834d747] 2026-06-18 09:35:53 +0530 --> + +《機械翻訳》«Avoid race condition while dropping obsolete replication slots » +(Xuneng Zhou) § + +《機械翻訳》«An incorrect unlock and log message could occur if another session immediately re-used the dropped slot's shared-memory entry.» @@ -2613,17 +3412,24 @@ Branch: REL_15_STABLE [a4eb59d40] 2026-06-03 18:47:46 +0900 Branch: REL_14_STABLE [968c50845] 2026-06-03 18:47:52 +0900 --> + +《機械翻訳》«Avoid race condition while dropping ephemeral replication slots » +(Zhijie Hou) § + +《機械翻訳》«The slot-releasing code performed some additional updates to the replication slot's shared-memory entry after releasing the slot. This is unsafe since another session could immediately re-use the dropped slot's shared-memory entry. Skip those updates in the case of an ephemeral slot.» @@ -2638,17 +3444,24 @@ Branch: REL_15_STABLE [b5f7e7569] 2026-05-13 11:46:21 +0900 Branch: REL_14_STABLE [e3c4e3746] 2026-05-13 11:46:26 +0900 --> + +《機械翻訳》«Fix stale progress reports during logical replication table synchronization » +(Shinya Kato) § + +《機械翻訳》«Previously, the pg_stat_progress_copy view in the subscriber would continue to show the initial COPY operation as active even after the data copy had finished. The stale entry remained visible until synchronization caught up with the publisher.» @@ -2667,17 +3480,24 @@ Branch: REL_16_STABLE [fffa4d870] 2026-07-06 09:48:01 +0900 Branch: REL_15_STABLE [a8fb98b7b] 2026-07-06 09:48:06 +0900 --> + +《機械翻訳》«Clear base backup progress on backup failure » +(Chao Li) § § + +《機械翻訳》«Previously the pg_stat_progress_basebackup view would continue to show a stale progress entry after a failure, until the replication client disconnected. pg_basebackup normally disconnects immediately, but other clients might not.» @@ -2699,9 +3519,13 @@ Branch: REL_16_STABLE [e34b1ff5d] 2026-06-23 07:59:02 +0900 Branch: REL_15_STABLE [9e4771825] 2026-06-23 07:59:03 +0900 --> + +《機械翻訳》«Fix possible PANIC due to concurrent drop of pgstats entries when track_functions is enabled » +(Sami Imseih, Michael Paquier) § § § @@ -2719,14 +3543,21 @@ Branch: REL_16_STABLE [636bcd6a5] 2026-08-07 14:23:38 +0900 Branch: REL_15_STABLE [10e20e59e] 2026-08-07 14:23:39 +0900 --> + +《機械翻訳》«Clean up broken local pgstats entry after failing to obtain space for the corresponding shared hashtable entry » +(Niall Newman) § + +《機械翻訳》«Failure to do this led to a null-pointer dereference the next time the local entry was used.» @@ -2737,8 +3568,12 @@ Branch: master Release: REL_19_BR [3048e8130] 2026-06-17 16:05:11 +0900 Branch: REL_18_STABLE [13f940b4b] 2026-06-17 16:05:37 +0900 --> + +《機械翻訳》«Avoid recording incorrect I/O operation statistics after a failed read or write » +(Bertrand Drouvot) § @@ -2754,9 +3589,13 @@ Branch: REL_15_STABLE [9b2a6ccc4] 2026-06-24 09:16:24 +0900 Branch: REL_14_STABLE [e520ad34b] 2026-06-24 09:17:36 +0900 --> + +《機械翻訳》«In PL/Perl, avoid NULL pointer dereference crash when working with an invalid PostgreSQL::InServer::ARRAY object » +(Xing Guo) § @@ -2773,14 +3612,21 @@ Branch: REL_14_STABLE [0b7719f74] 2026-06-29 11:44:35 +0900 Branch: REL_14_STABLE [309dc4526] 2026-06-29 14:21:15 +0900 --> + +《機械翻訳》«In PL/Python, properly check for errors when working with sequence and mapping objects » +(Richard Guo) § + +《機械翻訳》«Previously, a broken object or an unhandled exception could result in a NULL pointer dereference crash.» @@ -2817,9 +3663,13 @@ Branch: REL_15_STABLE [5ba13f8c0] 2026-07-09 18:36:18 +0300 Branch: REL_14_STABLE [7532f2117] 2026-07-09 18:36:41 +0300 --> + +《機械翻訳》«In libpq, always drain all pending bytes from the SSL or GSS decryption buffer during pqReadData() » +(Jacob Champion) § § § @@ -2827,9 +3677,12 @@ Branch: REL_14_STABLE [7532f2117] 2026-07-09 18:36:41 +0300 + +《機械翻訳》«This avoids edge cases where libpq or its calling application waits for more data to arrive on the socket, but actually all the data has already arrived.» @@ -2841,8 +3694,12 @@ Branch: REL_19_STABLE [8380013cd] 2026-08-04 17:05:37 +0900 Branch: REL_18_STABLE [6b46a5d1b] 2026-08-04 17:05:44 +0900 --> + +《機械翻訳》«Improve libpq's handling of out-of-memory conditions » +(Anthonin Bonnefoy) § @@ -2855,9 +3712,13 @@ Branch: REL_19_STABLE [0766bc57e] 2026-07-03 14:59:56 +0300 Branch: REL_18_STABLE [dd5eca055] 2026-07-03 15:00:00 +0300 --> + +《機械翻訳》«Fix libpq's trace facility to print new-style BackendKeyData and CancelRequest messages correctly » +(Anthonin Bonnefoy) § @@ -2873,16 +3734,23 @@ Branch: REL_15_STABLE [0f63b74a4] 2026-06-15 11:38:30 +0300 Branch: REL_14_STABLE [1b79c8d1a] 2026-06-15 11:38:40 +0300 --> + +《機械翻訳》«Allow libpq to accept ParameterDescription messages exceeding 30000 bytes » +(Ning Sun) § + +《機械翻訳》«Previously, this message type was not among those that libpq's validity heuristics believed could be long. The limit resulted in failure for prepared queries having more than 7498 parameters, which is unlikely but supported.» @@ -2893,15 +3761,22 @@ Branch: master Release: REL_19_BR [7f5e0b22e] 2026-06-25 16:58:29 -0400 Branch: REL_18_STABLE [917fdbc63] 2026-06-25 16:58:29 -0400 --> + +《機械翻訳》«Fix null-pointer crash in ecpg compiler » +(Jehan-Guillaume de Rorthais) § + +《機械翻訳》«ecpg failed on a DECLARE section containing a union nested inside a struct.» @@ -2916,16 +3791,23 @@ Branch: REL_15_STABLE [bfeddcf09] 2026-06-08 17:14:15 +0900 Branch: REL_14_STABLE [9e8fd9f7a] 2026-06-08 17:14:20 +0900 --> + +《機械翻訳》«Reject multiple descriptor header items in ecpg's GET/SET DESCRIPTOR statements » +(Masashi Kamura) § + +《機械翻訳》«Previously the grammar allowed this syntax, but broken C code was generated. Adjust the grammar and the documentation to allow only one header item.» @@ -2936,16 +3818,23 @@ Branch: master Release: REL_19_BR [d21604e17] 2026-06-03 08:58:26 +0900 Branch: REL_18_STABLE [1e9bc4074] 2026-06-03 08:58:29 +0900 --> + +《機械翻訳》«Fix issues with deferred errors in pipeline mode in psql » +(Michael Paquier) § + +《機械翻訳》«psql could get stuck or suffer an assertion failure in some scenarios where the server reports an error in response to a Sync message, such as a deferred constraint violation.» @@ -2960,14 +3849,21 @@ Branch: REL_15_STABLE [022ba5c61] 2026-06-08 14:38:00 +0900 Branch: REL_14_STABLE [a4ca91ea1] 2026-06-08 14:38:01 +0900 --> + +《機械翻訳》«Make line widths match in psql's expanded aligned output format » +(Pavel Stehule) § + +《機械翻訳》«When the table's data rows are narrower than the record header lines, widen the data rows to match the headers, avoiding unsightly output.» @@ -2978,16 +3874,23 @@ Branch: master Release: REL_19_BR [e04910a9a] 2026-05-18 08:33:36 -0700 Branch: REL_18_STABLE [e0c641ebb] 2026-05-18 08:33:36 -0700 --> + +《機械翻訳》«Enforce the intended upper limit for psql's special variable WATCH_INTERVAL » +(Sven Klemm, Daniel Gustafsson) § + +《機械翻訳》«If a too-large value was given, psql reported an error but applied the setting anyway.» @@ -3003,18 +3906,25 @@ Branch: REL_15_STABLE [e6e8a3078] 2026-07-25 19:10:49 +0900 Branch: REL_14_STABLE [4e19081da] 2026-07-25 19:11:34 +0900 --> + +《機械翻訳》«Fix psql's privilege check for showing database size in \l+ » +(Christoph Berg) § + +《機械翻訳》«The underlying server function permits users who have pg_read_all_stats privileges to see the sizes of all databases, even if they lack CONNECT privilege. But psql was unaware of that provision and would not call the function unless the user has CONNECT privilege.» @@ -3030,9 +3940,15 @@ Branch: REL_15_STABLE [c25737c89] 2026-07-03 13:50:26 +0900 Branch: REL_14_STABLE [2d44bb900] 2026-07-03 13:50:51 +0900 --> + +《マッチ度[59.016393]》psqlでのVACUUMオプション値に対するタブ補完を修正しました。 +(Yugo Nagata) +《機械翻訳》«Fix psql's tab completion for \df to consider procedures too » +(Erik Wienhold) § @@ -3047,16 +3963,23 @@ Branch: REL_16_STABLE [6432a4cd6] 2026-05-14 12:31:25 +0900 Branch: REL_15_STABLE [f18fcd9a4] 2026-05-14 12:31:43 +0900 --> + +《機械翻訳》«Fix thread-safety bug in pgbench » +(Fujii Masao) § + +《機械翻訳》«When pgbench runs with multiple threads and the option, different threads could attempt to use the same buffer to construct error messages, leading to corrupted log output.» @@ -3068,8 +3991,12 @@ Branch: REL_18_STABLE [d36b72894] 2026-06-29 13:01:57 +0200 Branch: REL_17_STABLE [090ce6934] 2026-06-29 13:02:07 +0200 --> + +《機械翻訳》«In pg_combinebackup, prevent infinite loop if the source file is shorter than expected » +(Peter Eisentraut) § @@ -3082,16 +4009,25 @@ Branch: REL_18_STABLE [196b4b5ae] 2026-05-27 10:35:18 +0900 Branch: REL_17_STABLE [c03784a21] 2026-05-27 10:35:49 +0900 --> + +《マッチ度[54.385965]》pg_createsubscriberにおいて、サブスクリプション名が正しくクォートされるようになりました。 +(Nathan Bossart) +《機械翻訳》«Fix cleanup of publisher-side objects after errors in pg_createsubscriber » +(Nisha Moond) § + +《機械翻訳》«When pg_createsubscriber fails after creating logical replication objects, it should remove the publication and replication slot that it created on the publisher. Some error cases failed to do so.» @@ -3106,15 +4042,22 @@ Branch: REL_15_STABLE [ba9833a75] 2026-05-20 15:57:14 +0900 Branch: REL_14_STABLE [5552a15a3] 2026-05-20 15:57:19 +0900 --> + +《機械翻訳》«Use the source cluster's group-read file permissions for pg_recvlogical output files » +(Fujii Masao) § + +《機械翻訳》«pg_recvlogical was documented to behave this way, but it never actually enabled group-read.» @@ -3127,19 +4070,26 @@ Branch: master Release: REL_19_BR [ae39bd23c] 2026-06-16 15:58:12 +0900 Branch: REL_18_STABLE [477efef08] 2026-06-16 15:58:17 +0900 --> + +《機械翻訳》«Fix inconsistent behavior of pg_restore with or » +(Chao Li, Michael Paquier) § § + +《機械翻訳》«When combined with other selective-restore options such as , these options failed to restore the expected items, unlike pg_dump with similar options.» @@ -3150,16 +4100,23 @@ Branch: master Release: REL_19_BR [d2cea6306] 2026-06-17 09:18:39 -0500 Branch: REL_18_STABLE [7e085aabd] 2026-06-17 09:18:39 -0500 --> - Fix vacuumdb --missing-stats-only to ignore + +《機械翻訳》«Fix vacuumdb --missing-stats-only to ignore partitioned expression indexes » +(Baji Shaik) § + +《機械翻訳》«Previously, vacuumdb would always attempt to ANALYZE the partitioned table, accomplishing nothing since statistics are never created for partitioned indexes, only for their leaf indexes.» @@ -3170,9 +4127,13 @@ Branch: master Release: REL_19_BR [389bd4c5b] 2026-06-12 09:37:37 +0900 Branch: REL_18_STABLE [12c32bbc8] 2026-06-12 09:39:19 +0900 --> + +《機械翻訳》«In contrib/amcheck, fix failure to report corruption of a btree metapage's allequalimage flag » +(Chao Li) § @@ -3185,8 +4146,12 @@ Branch: REL_19_STABLE [80cfd8aef] 2026-07-06 09:32:28 +0900 Branch: REL_18_STABLE [1f8ab91c1] 2026-07-06 09:32:30 +0900 --> + +《機械翻訳》«In contrib/amcheck, fix query-lifespan memory leak while verifying a GIN index » +(Kirill Reshke) § @@ -3202,14 +4167,21 @@ Branch: REL_15_STABLE [4284476c0] 2026-06-14 04:08:00 +0300 Branch: REL_14_STABLE [af09b18cb] 2026-06-14 04:06:43 +0300 --> + +《機械翻訳》«In contrib/amcheck, handle short-header varlena datums correctly » +(Andrey Borodin) § + +《機械翻訳》«This error could result in doing excess work while verifying a btree index, but seems not to have had any worse consequences.» @@ -3225,19 +4197,26 @@ Branch: REL_15_STABLE [98dd4406f] 2026-07-01 13:27:22 -0400 Branch: REL_14_STABLE [255bce448] 2026-07-01 13:27:22 -0400 --> + +《機械翻訳》«In contrib/btree_gist, fix NaN handling in the float4 and float8 opclasses » +(Bill Kim, Tom Lane) § + +《機械翻訳》«Comparisons, as well as the GiST penalty and distance functions, did not account for NaN and would give the wrong answer when handed one. It is recommended to reindex btree_gist indexes on float columns after installing this update, if there is any possibility that there are NaN entries in those columns.» @@ -3249,19 +4228,26 @@ Branch: REL_19_STABLE [11cb9c431] 2026-07-03 13:11:14 -0400 Branch: REL_18_STABLE [558c4ea9a] 2026-07-03 13:11:14 -0400 --> + +《機械翻訳》«In contrib/btree_gist, fix sorting of bit/varbit entries during GiST index construction » +(Tom Lane) § + +《機械翻訳》«Values of bit types were sorted as though they were byteas, which did not cause any obvious failure but would result in an inefficient index, since the types' representations are different. It is recommended to reindex btree_gist indexes on bit columns after installing this update.» @@ -3277,15 +4263,22 @@ Branch: REL_15_STABLE [8f2a1b3d3] 2026-07-03 13:50:14 -0400 Branch: REL_14_STABLE [286f9a3ce] 2026-07-03 13:50:14 -0400 --> + +《機械翻訳》«In contrib/btree_gist, fix searches using a not-equal operator » +(Ayush Tiwari) § + +《機械翻訳》«For variable-length data types, the code for scanning non-leaf index pages applied the wrong comparison function, leading to wrong results and potentially crashes.» @@ -3298,17 +4291,24 @@ Branch: master Release: REL_19_BR [5f5165e2f] 2026-05-26 00:51:18 +0900 Branch: REL_18_STABLE [130396e6c] 2026-05-26 00:52:38 +0900 --> + +《機械翻訳》«In contrib/dblink and contrib/postgres_fdw, ensure that a user-mapping setting for use_scram_passthrough overrides one for a foreign server » +(Matheus Alcantara) § § + +《機械翻訳》«Previously the precedence went the other way, but that is inconsistent with the behavior of other foreign-table options.» @@ -3319,16 +4319,23 @@ Branch: master Release: REL_19_BR [e2b881340] 2026-05-26 01:07:24 +0900 Branch: REL_18_STABLE [cd777e27e] 2026-05-26 01:08:47 +0900 --> + +《機械翻訳》«Reject setting use_scram_passthrough on contrib/dblink foreign-data wrappers » +(Matheus Alcantara) § + +《機械翻訳》«This option is only meaningful on foreign servers and user mappings, but dblink incorrectly allowed it at the FDW level as well (and then ignored it).» @@ -3349,20 +4356,27 @@ Branch: REL_15_STABLE [4fbbabb0a] 2026-06-18 12:22:55 -0400 Branch: REL_14_STABLE [1f6b2295f] 2026-06-18 12:22:55 -0400 --> + +《機械翻訳》«Fix unguarded recursion and loops in contrib/hstore_plperl, contrib/jsonb_plperl, and contrib/jsonb_plpython » +(Aleksander Alekseev) § § + +《機械翻訳》«Prevent stack overflow when dealing with deeply nested jsonb values, and allow interruption of the infinite loop caused when attempting to dereference circular chains of Perl object references.» @@ -3377,14 +4391,21 @@ Branch: REL_15_STABLE [702a6d5f6] 2026-05-25 18:15:49 -0400 Branch: REL_14_STABLE [a96b051a9] 2026-05-25 18:15:49 -0400 --> + +《機械翻訳》«Fix missed release of statistics catcache entry in contrib/intarray » +(Man Zeng) § + +《機械翻訳》«This oversight led to warnings like resource was not closed: cache pg_statistic @@ -3399,16 +4420,23 @@ Branch: REL_15_STABLE [1bec6b1c1] 2026-06-16 09:31:20 +0300 Branch: REL_14_STABLE [f528a5606] 2026-06-16 09:31:23 +0300 --> + +《機械翻訳》«In contrib/ltree, fix integer overflow in comparisons » +(Ayush Tiwari) § + +《機械翻訳》«ltree values containing more than about 14,653 labels resulted in wrong comparison answers due to overflow. If a btree index contains such values, it is probably corrupt and should be reindexed after installing this update.» @@ -3420,9 +4448,13 @@ Branch: REL_18_STABLE [020426268] 2026-06-22 12:59:16 -0400 Branch: REL_17_STABLE [2aa6be6e6] 2026-06-22 12:59:16 -0400 --> + +《機械翻訳》«In contrib/pgcrypto, avoid double-free crash after encountering an error while using an OSSLCipher object » +(Yuelin Wang) § @@ -3434,15 +4466,22 @@ Branch: master Release: REL_19_BR [dac36601f] 2026-06-26 19:47:36 +0200 Branch: REL_18_STABLE [3bf2cb225] 2026-06-26 19:48:20 +0200 --> + +《機械翻訳》«Fix out-of-bounds access in contrib/pg_prewarm's autoprewarm worker » +(Matheus Alcantara) § + +《機械翻訳》«The code tried to fetch a value from one past the end of an array, risking a segfault.» @@ -3457,16 +4496,23 @@ Branch: REL_15_STABLE [51f63ba2b] 2026-06-06 08:16:44 +0900 Branch: REL_14_STABLE [1eda3eb07] 2026-06-06 08:16:46 +0900 --> + +《機械翻訳》«Fix array overrun in contrib/pg_surgery's heap_force_kill and heap_force_freeze functions » +(Michael Paquier) § + +《機械翻訳》«Attempting to change a TID whose offset number equals MaxHeapTuplesPerPage wrote one byte past the end of the allocated array, potentially crashing the server.» @@ -3482,8 +4528,12 @@ Branch: REL_15_STABLE [395700f48] 2026-08-04 11:44:11 +0200 Branch: REL_14_STABLE [e9d53cf45] 2026-08-04 11:44:11 +0200 --> + +《機械翻訳》«In contrib/pg_surgery, avoid infinite loop with TID arrays having more than 64K elements » +(Andrey Rachitskiy) § @@ -3499,16 +4549,23 @@ Branch: REL_15_STABLE [77b2d18e9] 2026-05-14 13:11:49 -0500 Branch: REL_14_STABLE [1de0a711d] 2026-05-14 13:11:49 -0500 --> + +《機械翻訳》«Avoid NULL-pointer dereference in contrib/refint's check_foreign_key() » +(Ayush Tiwari) § + +《機械翻訳》«In the on-update-cascade case, a null value of a referenced column led to a crash. This is an oversight in the fix for CVE-2026-6637, but the code that was there before that wasn't really right either.» @@ -3523,19 +4580,26 @@ Branch: REL_15_STABLE [b3aa2083a] 2026-06-11 12:34:42 +0300 Branch: REL_14_STABLE [58b91fc73] 2026-06-11 12:34:45 +0300 --> + +《機械翻訳》«Fix contrib/seg to print segments with ~ certainty indicators correctly » +(Ewan Young) § + +《機械翻訳》«Due to a typo, seg_out() did not print a ~ certainty indicator attached to a segment's upper boundary. Worse, if the lower boundary had ~ while the upper boundary had no indicator, the upper boundary was not printed at all, incorrectly converting the value into an open interval.» @@ -3550,9 +4614,13 @@ Branch: REL_15_STABLE [bc5775149] 2026-06-11 14:29:28 +0900 Branch: REL_14_STABLE [f3f901a53] 2026-06-11 14:29:29 +0900 --> + +《機械翻訳》«Fix crash with namespace nodes in contrib/xml2's xpath_nodeset() function » +(Andrey Chernyy, Michael Paquier) § @@ -3568,8 +4636,12 @@ Branch: REL_15_STABLE [c7f70fa1b] 2026-06-12 13:57:22 +0200 Branch: REL_14_STABLE [086652c02] 2026-06-12 13:57:22 +0200 --> + +《機械翻訳》«Support building PostgreSQL with OpenSSL 4 » +(Daniel Gustafsson) § @@ -3586,22 +4658,34 @@ Branch: REL_15_STABLE [af7be5672] 2026-08-02 11:26:30 -0400 Branch: REL_14_STABLE [812cc1a73] 2026-08-02 11:26:30 -0400 --> + +《マッチ度[85.393258]》タイムゾーンデータファイルがtzdataリリース2026bに更新されました。 +(Tom Lane) +《機械翻訳》«Update time zone data files to tzdata release 2026c » +(Tom Lane) § + +《機械翻訳》«Alberta (America/Edmonton) will be on year-round UTC-06 (effectively, permanent DST) beginning in November 2026. This release assumes that their TZ abbreviation will be CST from that time forward. That seems likely to change, but it's unclear what new abbreviation will be used.» + +《機械翻訳》«Morocco (Africa/Casablanca) will move to permanent UTC+00, without daylight saving time transitions, on 2026-09-20.» @@ -4096,11 +5180,16 @@ Branch: REL_14_STABLE [b282280e9] 2026-05-11 05:13:51 -0700 + +《マッチ度[89.389068]》パスワードやハッシュなどの検証には、 memcpy()strcmp()の代わりにtimingsafe_bcmp()を使用するようになりました。 +これらの関数のデータ依存性が、これらの箇所で悪用される可能性があるかどうかは不明ですが、安全を期してこれらが置き換えられました。 +《機械翻訳》«Use timingsafe_bcmp() instead of memcmp() or strcmp() when checking passwords, hashes, etc. It is not known whether the data dependency of those functions is usefully exploitable in any of these places, but in the interests of safety, replace them.» diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml index 103cc49cf96..fc92ebeec02 100644 --- a/doc/src/sgml/storage.sgml +++ b/doc/src/sgml/storage.sgml @@ -1239,6 +1239,7 @@ data. Empty in ordinary tables. + +《マッチ度[95.295613]》それぞれのページの最初の24バイトはページヘッダ(PageHeaderData)から構成されています。 +その書式をにて説明します。 +最初のフィールドは、このページに関連する最も最近のWAL項目を表しています。 +2番目のフィールドにはが有効な場合にページチェックサムが格納されています。 +次にフラグビットを含む2バイトのフィールドがあります。 +その後に2バイトの整数フィールドが3つ続きます(pd_lowerpd_upperpd_special)。 +これらには、割り当てられていない空間の始まり、割り当てられていない空間の終わり、そして特別な空間の始まりのバイトオフセットが格納されています。 +ページヘッダの次の2バイトであるpd_pagesize_versionは、ページサイズとバージョン指示子の両方を格納します。 +PostgreSQL 8.3以降のバージョン番号は4、PostgreSQL 8.1と8.2のバージョン番号は3、PostgreSQL 8.0のバージョン番号は2、PostgreSQL 7.3と7.4のバージョン番号は1です。 +それより前のリリースのバージョン番号は0です。 +(ほとんどのバージョン間で基本的なページレイアウトやヘッダの書式は変更されていませんが、ヒープ行ヘッダのレイアウトが変更されました。) +ページサイズは基本的に照合用としてのみ存在しています。 +同一インストレーションでの複数のページサイズはサポートされていません。 +最後のフィールドはそのページの切り詰めが有益かどうかを示すヒントです。 +これはページ上で切り詰められていないもっとも古いXMAXが追跡するものです。 From 9bb18513d397d52b5bcbcbfdf614e1aa0eeb7ed1 Mon Sep 17 00:00:00 2001 From: Noboru Saito Date: Thu, 13 Aug 2026 23:25:43 +0900 Subject: [PATCH 248/250] =?UTF-8?q?18.6=E7=BF=BB=E8=A8=B3=E6=BA=96?= =?UTF-8?q?=E5=82=993=20=E6=A9=9F=E6=A2=B0=E7=BF=BB=E8=A8=B3=E5=89=8D?= =?UTF-8?q?=E3=81=AB=E6=89=8B=E5=8B=95=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/src/sgml/ref/drop_subscription.sgml | 12 +- doc/src/sgml/release-18.sgml | 145 +++++++++------------- doc/src/sgml/stylesheet-speedup-xhtml.xsl | 7 ++ 3 files changed, 74 insertions(+), 90 deletions(-) diff --git a/doc/src/sgml/ref/drop_subscription.sgml b/doc/src/sgml/ref/drop_subscription.sgml index 92757caba43..8ce8a8bb03a 100644 --- a/doc/src/sgml/ref/drop_subscription.sgml +++ b/doc/src/sgml/ref/drop_subscription.sgml @@ -136,8 +136,6 @@ DROP SUBSCRIPTION [ IF EXISTS ] name ALTER SUBSCRIPTION ... DISABLE, and then disassociate it from the replication slot by executing ---> -《機械翻訳》«When dropping a subscription that is associated with a replication slot on the remote host (the normal state), DROP SUBSCRIPTION will connect to the remote host and try to drop the replication slot (and any remaining table synchronization slots) as part of its operation. This is necessary so that the resources allocated for the subscription on the remote host are released. If this fails, either because the remote host is not reachable or because the remote replication slot cannot be dropped or does not exist or never existed, the DROP SUBSCRIPTION command will fail. To proceed in this situation, first disable the subscription by executing ALTER SUBSCRIPTION ... DISABLE, and then disassociate it from the replication slot by executing» ALTER SUBSCRIPTION ... SET (slot_name = NONE). After that, DROP SUBSCRIPTION will not attempt to drop @@ -148,6 +146,16 @@ DROP SUBSCRIPTION [ IF EXISTS ] name. +--> +《マッチ度[82.352941]》リモートホストのレプリケーションスロットに紐付けられているサブスクリプション(これが通常の状態です)を削除するとき、DROP SUBSCRIPTIONはその操作の一部として、リモートホストに接続し、レプリケーションスロット(と残りのテーブル同期スロット)を削除しようとします。 +リモートホスト上でサブスクリプションに割り当てられたリソースを解放するために、これが必要となります。 +リモートホストに到達できない、あるいはリモートのレプリケーションスロットが削除できない、存在しない、存在したことがない、という理由で削除に失敗した場合、DROP SUBSCRIPTIONコマンドは失敗します。 +この状況において先へ進むためには、まずALTER SUBSCRIPTION ... DISABLEを実行してサブスクリプションを無効にし、それからALTER SUBSCRIPTION ... SET (slot_name = NONE)を実行してサブスクリプションとレプリケーションスロットの紐付けを解除してください。 +その後ならDROP SUBSCRIPTIONはリモートホスト上で何のアクションも起こそうとしません。 +リモートのレプリケーションスロットがそれでも存在する場合、それ (と関連するテーブル同期スロット) を手作業で削除すべきであることに注意してください。 +そうしなければ、WALを保存し続け、最終的にはディスクを一杯にしてしまうかもしれません。 +も参照してください。 +《機械翻訳》«When dropping a subscription that is associated with a replication slot on the remote host (the normal state), DROP SUBSCRIPTION will connect to the remote host and try to drop the replication slot (and any remaining table synchronization slots) as part of its operation. This is necessary so that the resources allocated for the subscription on the remote host are released. If this fails, either because the remote host is not reachable or because the remote replication slot cannot be dropped or does not exist or never existed, the DROP SUBSCRIPTION command will fail. To proceed in this situation, first disable the subscription by executing ALTER SUBSCRIPTION ... DISABLE, and then disassociate it from the replication slot by executing ALTER SUBSCRIPTION ... SET (slot_name = NONE). After that, DROP SUBSCRIPTION will not attempt to drop the subscription's own replication slot. It may still connect to the publisher to drop internally-created table synchronization slots if some table synchronization is left unfinished; if the publisher is unreachable, those slots (and the main slot, if it still exists) must be dropped manually. Otherwise it/they will continue to reserve WAL and might eventually cause the disk to fill up. See also diff --git a/doc/src/sgml/release-18.sgml b/doc/src/sgml/release-18.sgml index 03bd8a9c2d1..8b0123dfe86 100644 --- a/doc/src/sgml/release-18.sgml +++ b/doc/src/sgml/release-18.sgml @@ -31,7 +31,10 @@ + + バージョン18.6への移行 -《マッチ度[85.416667]》しかしながら、18.2より前のバージョンからアップグレードする場合は、を参照してください。 《機械翻訳》«Also, if you are upgrading from a version earlier than 18.2, see @@ -156,9 +158,8 @@ output_plugin_libraries = 'pgoutput, test_decoding, my_trusted_decoder' for reporting this problem. (CVE-2026-6471) --> -《マッチ度[75.757576]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 -(CVE-2026-6476) -《機械翻訳》«The PostgreSQL Project thanks Vladimir Tokarev and Yu Kunpeng for reporting this problem. (CVE-2026-6471)» +《機械翻訳》«The PostgreSQL Project thanks Vladimir Tokarev and Yu Kunpeng for reporting this problem. » +(CVE-2026-6471) @@ -240,9 +241,8 @@ pgp_sym_decrypt(encrypted_column, any key, 'ignore-ci for reporting this problem. (CVE-2026-14663) --> -《マッチ度[74.137931]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 -(CVE-2026-6476) -《機械翻訳》«The PostgreSQL Project thanks Shishir Sharma for reporting this problem. (CVE-2026-14663)» +《機械翻訳》«The PostgreSQL Project thanks Shishir Sharma for reporting this problem. » +(CVE-2026-14663) @@ -310,9 +310,8 @@ Branch: REL_14_STABLE [7215c7e96] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-6464) --> -《マッチ度[75.213675]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 -(CVE-2026-6474) -《機械翻訳》«The PostgreSQL Project thanks Alexander Lakhin for reporting this problem. (CVE-2026-6464)» +《機械翻訳》«The PostgreSQL Project thanks Alexander Lakhin for reporting this problem. » +(CVE-2026-6464) @@ -357,9 +356,8 @@ Branch: REL_14_STABLE [1f49beef2] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-16239) --> -《マッチ度[70.967742]》PostgreSQLプロジェクトは、本問題を報告してくれたCalif.io(ClaudeおよびAnthropic Researchとの協力)に感謝します。 -(CVE-2026-6479) -《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) and Peter Geoghegan for reporting this problem. (CVE-2026-16239)» +《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) and Peter Geoghegan for reporting this problem. » +(CVE-2026-16239) @@ -441,9 +439,8 @@ Branch: REL_14_STABLE [890327639] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14664) --> -《マッチ度[73.949580]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 -(CVE-2026-6474) -《機械翻訳》«The PostgreSQL Project thanks Francesco Verardi for reporting this problem. (CVE-2026-14664)» +《機械翻訳》«The PostgreSQL Project thanks Francesco Verardi for reporting this problem. » +(CVE-2026-14664) @@ -484,9 +481,8 @@ Branch: REL_14_STABLE [42d333a78] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-18024) --> -《マッチ度[71.559633]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 -(CVE-2026-6474) -《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. (CVE-2026-18024)» +《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. » +(CVE-2026-18024) @@ -527,9 +523,8 @@ Branch: REL_18_STABLE [8d428c6e6] 2026-08-10 06:38:12 -0700 for reporting this problem. (CVE-2026-16238) --> -《マッチ度[68.613139]》PostgreSQLプロジェクトは、本問題を報告してくれたPavel Kohoutに感謝します。 -(CVE-2026-6638) -《機械翻訳》«The PostgreSQL Project thanks Amy Burnett (OpenAI Codex Security) for reporting this problem. (CVE-2026-16238)» +《機械翻訳》«The PostgreSQL Project thanks Amy Burnett (OpenAI Codex Security) for reporting this problem. » +(CVE-2026-16238) @@ -570,9 +565,8 @@ Branch: REL_14_STABLE [59205c7e9] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14668) --> -《マッチ度[72.477064]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 -(CVE-2026-6476) -《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. (CVE-2026-14668)» +《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. » +(CVE-2026-14668) @@ -620,9 +614,8 @@ Branch: REL_14_STABLE [1e3014f37] 2026-08-10 06:38:34 -0700 for reporting these problems. (CVE-2026-14662) --> -《マッチ度[65.333333]》PostgreSQLプロジェクトは、本問題を報告してくれたYu KunpengとMartin Heistermannに感謝します。 -(CVE-2026-6477) -《機械翻訳》«The PostgreSQL Project thanks Yuhang Wu, Zhenpeng Lin, Zheng Yu, and Hcamael for reporting these problems. (CVE-2026-14662)» +《機械翻訳》«The PostgreSQL Project thanks Yuhang Wu, Zhenpeng Lin, Zheng Yu, and Hcamael for reporting these problems. » +(CVE-2026-14662) @@ -672,9 +665,8 @@ Branch: REL_14_STABLE [c7f462838] 2026-08-10 06:38:35 -0700 for reporting these problems. (CVE-2026-14679) --> -《マッチ度[65.972222]》PostgreSQLプロジェクトは、本問題を報告してくれたYu KunpengとMartin Heistermannに感謝します。 -(CVE-2026-6477) -《機械翻訳》«The PostgreSQL Project thanks Zheng Yu, ylwangtju, and Masahiko Sawada for reporting these problems. (CVE-2026-14679)» +《機械翻訳》«The PostgreSQL Project thanks Zheng Yu, ylwangtju, and Masahiko Sawada for reporting these problems. » +(CVE-2026-14679) @@ -722,9 +714,8 @@ Branch: REL_14_STABLE [913fe0c31] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14680) --> -《マッチ度[67.883212]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 -(CVE-2026-6473) -《機械翻訳》«The PostgreSQL Project thanks Amy Burnett (OpenAI Codex Security) for reporting this problem. (CVE-2026-14680)» +《機械翻訳》«The PostgreSQL Project thanks Amy Burnett (OpenAI Codex Security) for reporting this problem. » +(CVE-2026-14680) @@ -764,9 +755,8 @@ Branch: REL_14_STABLE [70a3b4e18] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-6469) --> -《マッチ度[75.675676]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 -(CVE-2026-6473) -《機械翻訳》«The PostgreSQL Project thanks Noah Misch for reporting this problem. (CVE-2026-6469)» +《機械翻訳》«The PostgreSQL Project thanks Noah Misch for reporting this problem. » +(CVE-2026-6469) @@ -809,9 +799,8 @@ Branch: REL_14_STABLE [967acab87] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-15741) --> -《マッチ度[78.915663]》PostgreSQLプロジェクトは、本問題を報告してくれたCalif.io(ClaudeおよびAnthropic Researchとの協力)に感謝します。 -(CVE-2026-6479) -《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) for reporting this problem. (CVE-2026-15741)» +《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) for reporting this problem. » +(CVE-2026-15741) @@ -871,9 +860,8 @@ Branch: REL_14_STABLE [1a358b8f2] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-6470) --> -《マッチ度[75.892857]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 -(CVE-2026-6476) -《機械翻訳》«The PostgreSQL Project thanks Jingzhou Fu for reporting this problem. (CVE-2026-6470)» +《機械翻訳》«The PostgreSQL Project thanks Jingzhou Fu for reporting this problem.» + (CVE-2026-6470) @@ -915,9 +903,8 @@ Branch: REL_14_STABLE [f4174aa84] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-14666) --> -《マッチ度[69.172932]》PostgreSQLプロジェクトは、本問題を報告してくれたPavel Kohoutに感謝します。 -(CVE-2026-6638) -《機械翻訳》«The PostgreSQL Project thanks Ilya Staroverov and Shinya Kato for reporting this problem. (CVE-2026-14666)» +《機械翻訳》«The PostgreSQL Project thanks Ilya Staroverov and Shinya Kato for reporting this problem. » +(CVE-2026-14666) @@ -957,9 +944,8 @@ Branch: REL_17_STABLE [067a64d40] 2026-08-10 06:38:18 -0700 for reporting this problem. (CVE-2026-14681) --> -《マッチ度[71.028037]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 -(CVE-2026-6476) -《機械翻訳》«The PostgreSQL Project thanks p4p3r for reporting this problem. (CVE-2026-14681)» +《機械翻訳》«The PostgreSQL Project thanks p4p3r for reporting this problem. » +(CVE-2026-14681) @@ -1002,9 +988,8 @@ Branch: REL_16_STABLE [fadbe882d] 2026-08-10 06:38:24 -0700 for reporting this problem. (CVE-2026-14672) --> -《マッチ度[75.221239]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 -(CVE-2026-6473) -《機械翻訳》«The PostgreSQL Project thanks Radim Marek for reporting this problem. (CVE-2026-14672)» +《機械翻訳》«The PostgreSQL Project thanks Radim Marek for reporting this problem. » +(CVE-2026-14672) @@ -1047,9 +1032,8 @@ Branch: REL_14_STABLE [74c59d062] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-16241) --> -《マッチ度[72.972973]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 -(CVE-2026-6473) -《機械翻訳》«The PostgreSQL Project thanks ylwangtju for reporting this problem. (CVE-2026-16241)» +《機械翻訳》«The PostgreSQL Project thanks ylwangtju for reporting this problem. » +(CVE-2026-16241) @@ -1093,9 +1077,8 @@ Branch: REL_14_STABLE [2006fca40] 2026-08-10 06:38:37 -0700 for reporting this problem. (CVE-2026-18408) --> -《マッチ度[65.068493]》PostgreSQLプロジェクトは、本問題を報告してくれたAltan Birlerに感謝します。 -(CVE-2026-2003) -《機械翻訳》«The PostgreSQL Project thanks Lucas Velgus, Filip Janus, and Daniel Bakker for reporting this problem. (CVE-2026-18408)» +《機械翻訳》«The PostgreSQL Project thanks Lucas Velgus, Filip Janus, and Daniel Bakker for reporting this problem. » +(CVE-2026-18408) @@ -1143,9 +1126,8 @@ Branch: REL_14_STABLE [57aa21f69] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-19385) --> -《マッチ度[74.358974]》PostgreSQLプロジェクトは、本問題を報告してくれたJoe Conwayに感謝します。 -(CVE-2026-6478) -《機械翻訳》«The PostgreSQL Project thanks Masahiko Sawada for reporting this problem. (CVE-2026-19385)» +《機械翻訳》«The PostgreSQL Project thanks Masahiko Sawada for reporting this problem. » +(CVE-2026-19385) @@ -1186,9 +1168,8 @@ Branch: REL_14_STABLE [d7fcfead3] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14670) --> -《マッチ度[72.477064]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 -(CVE-2026-6476) -《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. (CVE-2026-14670)» +《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. » +(CVE-2026-14670) @@ -1229,9 +1210,8 @@ Branch: REL_14_STABLE [aff9dac1c] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14677) --> -《マッチ度[63.313609]》PostgreSQLプロジェクトは、本問題を報告してくれたPositive TechnologiesのAleksey Solovevに感謝します。 -(CVE-2025-12818) -《機械翻訳》«The PostgreSQL Project thanks the Tulya Project (Team Dhiutsa, Bitecope Technologies Private Ltd) for reporting this problem. (CVE-2026-14677)» +《機械翻訳》«The PostgreSQL Project thanks the Tulya Project (Team Dhiutsa, Bitecope Technologies Private Ltd) for reporting this problem. » +(CVE-2026-14677) @@ -1277,9 +1257,8 @@ Branch: REL_14_STABLE [39d792040] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-14673) --> -《マッチ度[70.992366]》PostgreSQLプロジェクトは、本問題を報告してくれたJoe Conwayに感謝します。 -(CVE-2026-6478) -《機械翻訳》«The PostgreSQL Project thanks Yuelin Wang and Jacob Brazeal for reporting this problem. (CVE-2026-14673)» +《機械翻訳》«The PostgreSQL Project thanks Yuelin Wang and Jacob Brazeal for reporting this problem. » +(CVE-2026-14673) @@ -1322,9 +1301,8 @@ Branch: REL_14_STABLE [9505175f2] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-15742) --> -《マッチ度[78.915663]》PostgreSQLプロジェクトは、本問題を報告してくれたCalif.io(ClaudeおよびAnthropic Researchとの協力)に感謝します。 -(CVE-2026-6479) -《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) for reporting this problem. (CVE-2026-15742)» +《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) for reporting this problem. » +(CVE-2026-15742) @@ -1360,9 +1338,8 @@ Branch: REL_18_STABLE [8a31ffc2d] 2026-08-10 06:38:11 -0700 for reporting this problem. (CVE-2026-14676) --> -《マッチ度[60.240964]》PostgreSQLプロジェクトは、本問題を報告してくれたYu KunpengとMartin Heistermannに感謝します。 -(CVE-2026-6477) -《機械翻訳》«The PostgreSQL Project thanks Sajeeb Lohani (with TrendAI Zero Day Initiative) and Yuelin Wang for reporting this problem. (CVE-2026-14676)» +《機械翻訳》«The PostgreSQL Project thanks Sajeeb Lohani (with TrendAI Zero Day Initiative) and Yuelin Wang for reporting this problem. » +(CVE-2026-14676) @@ -1403,9 +1380,8 @@ Branch: REL_14_STABLE [a74aa0854] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14678) --> -《マッチ度[75.862069]》PostgreSQLプロジェクトは、本問題を報告してくれたXint Codeに感謝します。 -(CVE-2026-6473) -《機械翻訳》«The PostgreSQL Project thanks Mehmet D. Ince for reporting this problem. (CVE-2026-14678)» +《機械翻訳》«The PostgreSQL Project thanks Mehmet D. Ince for reporting this problem. » +(CVE-2026-14678) @@ -1447,9 +1423,8 @@ Branch: REL_14_STABLE [5b72d0279] 2026-06-05 12:08:05 -0500 for reporting this problem. (CVE-2026-14671) --> -《マッチ度[72.477064]》PostgreSQLプロジェクトは、本問題を報告してくれたYu Kunpengに感謝します。 -(CVE-2026-6476) -《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. (CVE-2026-14671)» +《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. » +(CVE-2026-14671) @@ -3945,8 +3920,6 @@ Branch: REL_14_STABLE [2d44bb900] 2026-07-03 13:50:51 +0900 for \df to consider procedures too (Erik Wienhold) --> -《マッチ度[59.016393]》psqlでのVACUUMオプション値に対するタブ補完を修正しました。 -(Yugo Nagata) 《機械翻訳》«Fix psql's tab completion for \df to consider procedures too » (Erik Wienhold) § @@ -4013,8 +3986,6 @@ Branch: REL_17_STABLE [c03784a21] 2026-05-27 10:35:49 +0900 Fix cleanup of publisher-side objects after errors in pg_createsubscriber (Nisha Moond) --> -《マッチ度[54.385965]》pg_createsubscriberにおいて、サブスクリプション名が正しくクォートされるようになりました。 -(Nathan Bossart) 《機械翻訳》«Fix cleanup of publisher-side objects after errors in pg_createsubscriber » (Nisha Moond) § @@ -4662,8 +4633,6 @@ Branch: REL_14_STABLE [812cc1a73] 2026-08-02 11:26:30 -0400 Update time zone data files to tzdata release 2026c (Tom Lane) --> -《マッチ度[85.393258]》タイムゾーンデータファイルがtzdataリリース2026bに更新されました。 -(Tom Lane) 《機械翻訳》«Update time zone data files to tzdata release 2026c » (Tom Lane) § diff --git a/doc/src/sgml/stylesheet-speedup-xhtml.xsl b/doc/src/sgml/stylesheet-speedup-xhtml.xsl index 0e8e97c7e5c..3befd1ee082 100644 --- a/doc/src/sgml/stylesheet-speedup-xhtml.xsl +++ b/doc/src/sgml/stylesheet-speedup-xhtml.xsl @@ -262,6 +262,7 @@ + + + + + + From f7e93aad69e5262c6a448ed4f30d63701cad18ed Mon Sep 17 00:00:00 2001 From: Noboru Saito Date: Thu, 13 Aug 2026 23:40:32 +0900 Subject: [PATCH 249/250] =?UTF-8?q?18.6=E7=BF=BB=E8=A8=B3=E6=BA=96?= =?UTF-8?q?=E5=82=994=20release-18.sgml=E4=BB=A5=E5=A4=96=E3=81=AE?= =?UTF-8?q?=E6=A9=9F=E6=A2=B0=E7=BF=BB=E8=A8=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=A8=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/src/sgml/amcheck.sgml | 3 +- doc/src/sgml/config.sgml | 49 +++++++++---- doc/src/sgml/config0.sgml | 43 ++++++++--- doc/src/sgml/config1.sgml | 94 ++++++++++++++++++++++++- doc/src/sgml/config2.sgml | 11 +-- doc/src/sgml/config3.sgml | 5 +- doc/src/sgml/contrib-spi.sgml | 17 +++-- doc/src/sgml/dblink.sgml | 7 +- doc/src/sgml/ddl.sgml | 8 ++- doc/src/sgml/ecpg.sgml | 6 +- doc/src/sgml/func.sgml | 12 +++- doc/src/sgml/func1.sgml | 6 +- doc/src/sgml/func2.sgml | 18 +++-- doc/src/sgml/func4.sgml | 17 ----- doc/src/sgml/libpq2.sgml | 6 +- doc/src/sgml/logical-replication.sgml | 7 +- doc/src/sgml/maintenance.sgml | 16 +++-- doc/src/sgml/oauth-validators.sgml | 3 +- doc/src/sgml/pgcrypto.sgml | 12 +++- doc/src/sgml/postgres-fdw.sgml | 6 +- doc/src/sgml/ref/alter_table.sgml | 10 ++- doc/src/sgml/ref/copy.sgml | 3 +- doc/src/sgml/ref/create_type.sgml | 2 +- doc/src/sgml/ref/drop_subscription.sgml | 11 ++- doc/src/sgml/ref/pg_recvlogical.sgml | 2 +- doc/src/sgml/ref/psql-ref.sgml | 8 ++- 26 files changed, 299 insertions(+), 83 deletions(-) diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml index 65ebc972db3..2cdffefeeab 100644 --- a/doc/src/sgml/amcheck.sgml +++ b/doc/src/sgml/amcheck.sgml @@ -592,7 +592,8 @@ B-Tree検証関数のheapallindexed引数がtrue --> 《マッチ度[70.621469]》amcheckは、データチェックサムが検知できないような、様々なタイプの障害モードを効果的に検知できます。 以下のようなものがあります。 -《機械翻訳》«amcheck can be effective at detecting various types of failure modes that data checksums will fail to catch. These include:» +《機械翻訳》amcheckデータチェックサムがキャッチに対して失敗するさまざまなタイプの障害モードを検出するのに効果的です。 +これらのinclude:。 diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 0ed4e477f55..bd0f0036a87 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1767,7 +1767,9 @@ SCRAM-SHA-256を使用してパスワードを暗号化するときに実行さ specified in the postgresql.conf file or on the server command line. --> -《機械翻訳》«If a role password was created with a different iteration count than the value of scram_iterations specified in the postgresql.conf file or on the server command line, an unauthenticated user can discern the existence of the role by observing discrepancies in the server's responses to connection attempts. If you find this concerning, ensure that all role passwords are created with scram_iterations set to the value specified in the postgresql.conf file or on the server command line.» +《機械翻訳》ロールパスワードがpostgresql.confカウントまたはファイルで指定されたの値とは異なる反復サーバコマンドラインで作成された場合、認証されていないユーザは、コネクションの試行に対するサーバの応答の不一致を観察することでロールの存在を識別できます。 +これが問題となる場合は、すべてのロールパスワードがpostgresql.confファイルまたはサーバコマンドラインで指定された値に設定されたで作成された保証を確認します。 +scram_iterations scram_iterations @@ -2348,7 +2350,12 @@ TLSバージョン1.3を使用する接続についてはOpenSSL名は、prime256v1(NIST P-256)、secp384r1(NIST P-384)、およびsecp521r1(NIST P-521)です。 openssl ecparam -list_curvesコマンドを使用すると、使用可能なグループの不完全なリストを表示することができます。 ただし、すべてがTLSで使用できるわけではなく、サポートされているグループ名と別名の多くは省略されています。 -《機械翻訳》«OpenSSL names for the most common groups are: prime256v1 (NIST P-256), secp384r1 (NIST P-384), secp521r1 (NIST P-521). An incomplete list of available groups can be shown with the command openssl ecparam -list_curves. Not all of them are usable with TLS though, and many supported group names and aliases are omitted.» +《機械翻訳》最も一般的なグループのOpenSSL名は、prime256v1 NIST P-256、secp384r1 NIST P-384、secp521r1 NIST P-521です。 +使用可能なグループの不完全なリストは、コマンドOpenSSL ecparam -list_curvesで表示できます。 +すべてのグループがTLSで使用できるわけではありませんが、サポートされているSSL名とエイリアスの多くは省略されています。 @@ -5242,7 +5251,13 @@ WALの更新をディスクへ強制するのに使用される方法です。 サポートされている方法はpglzlz4PostgreSQLでコンパイルされた場合)およびzstdPostgreSQLでコンパイルされた場合)です。 デフォルト値はoffです。 スーパーユーザと適切なSET権限を持つユーザのみがこの設定を変更できます。 -《機械翻訳》«This parameter enables compression of WAL using the specified compression method. When enabled, the PostgreSQL server compresses full page images written to WAL (e.g. when is on, during a base backup, etc.). A compressed page image will be decompressed during WAL replay. The supported methods are pglz, lz4 (if PostgreSQL was compiled with ) and zstd (if PostgreSQL was compiled with ). The value on is a historical spelling of pglz. The default value is off. Only superusers and users with the appropriate SET privilege can change this setting.» +《機械翻訳》このパラメータは、指定された圧縮メソッドを使用したWALの圧縮を有効にします。 +有効になっている場合、PostgreSQLサーバはWALに書き込まれたフルページイメージを圧縮しますがオンの場合、ベースバックアップ中など。 +圧縮されたページイメージはWALリプレイ中に解凍されます。 +サポートされている方法は、pglz,lz4PostgreSQLでコンパイルされた場合とzstdPostgreSQLでコンパイルされた場合です。 +値onpglzの歴史的な綴りです。 +デフォルト値はoffです。 +スーパーユーザと適切なSET権限を持つユーザのみがこの設定を変更できます。 @@ -6960,7 +6975,11 @@ WAL要約は、先行するバックアップと新しいバックアップの which are the two logical output plugins included in the standard PostgreSQL distribution. --> -《機械翻訳》«Lists the libraries installed in that are also trusted for use as logical output plugins by replication clients. Any logical decoding or replication requests for other libraries will be refused. All users are subject to this restriction. The default is 'pgoutput, test_decoding', which are the two logical output plugins included in the standard PostgreSQL distribution.» +《機械翻訳》にインストールされているライブラリのうち、トラステッドクライアントがロジカルのアウトプットプラグインとして使用するためにレプリケーションでもあるライブラリを一覧表示します。 +他のライブラリに対するロジカルデコーディングまたはレプリケーションの要求は拒否されます。 +すべてのユーザはこの制限のサブジェクトです。 +デフォルトは'pgoutput, test_decoding'です。 +これらは標準PostgreSQLディストリビューションに含まれている2つのロジカルアウトプットプラグインです。 -《機械翻訳》«The format is a comma-separated list of library names, where each name is interpreted as for the LOAD command (but logical decoding clients must specify a plugin name that exactly matches an entry in the list, without variations in case or path structure). Whitespace between entries is ignored; surround a library name with double quotes if you need to include whitespace or commas in the name.» +《機械翻訳》フォーマットは、カンマで区切られたライブラリ名のリストです。 +各名前はLOADコマンドと同様に解釈されます(ただし、ロジカルデコーディングクライアントは、ケースやパスの構造に違いがなく、リスト内のエントリと正確に一致するプラグイン名前を指定する必要があります)。 +エントリ間の空白は無視されます。 +名前内でincludeの空白またはカンマが必要な場合は、ライブラリ名前を二重引用符で囲みます。 -《機械翻訳》«It is the responsibility of the server administrator to ensure that libraries added to this list do not unintentionally give additional privileges to non-superusers when they are loaded into the server.» +《機械翻訳》このサーバに追加されたライブラリが保証にロードされる際に、非スーパーユーザに意図せず追加の権限を与えないようにすることは、リストの管理者の責任です。 +サーバ @@ -6990,7 +7013,7 @@ WAL要約は、先行するバックアップと新しいバックアップの query can help construct the list of plugins that are required by all persistent logical replication slots: --> -《機械翻訳》«When updating the server from a version that does not have the output_plugin_libraries parameter, the following query can help construct the list of plugins that are required by all persistent logical replication slots:» +《機械翻訳》output_plugin_libraries更新を持たないバージョンからサーバをパラメータする場合、次の問い合わせは、すべての永続論理レプリケーションスロットで必要とされるプラグインのリストをヘルプコンストラクトすることができます。 SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL; @@ -6998,7 +7021,7 @@ SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL; Review the list carefully for safety before adjusting output_plugin_libraries. --> -《機械翻訳》«Review the list carefully for safety before adjusting output_plugin_libraries.» +《機械翻訳》安全前調整のためにリストを注意深くレビューしてくださいoutput_plugin_libraries -《機械翻訳》«The above query can only display plugins which were successfully added to replication slots at some point in the past. Newly refused requests will appear in the logs with a message similar to» +《機械翻訳》上記の問い合わせでは、過去に一部のディスプレイのレプリケーションスロットに正常に追加されたプラグインのみをポイントで使用できます。 +新たに拒否されたリクエストは、次のようなメッセージでログに表示されます。 ERROR: library "..." may not be used as an output plugin DETAIL: The configuration parameter "output_plugin_libraries" (currently 'pgoutput, test_decoding') does not name this library as a trusted output plugin. @@ -12203,7 +12227,8 @@ log_line_prefix = '%m [%p] %q%u@%d/%a ' 《マッチ度[82.594937]》この設定は、および関連設定の結果を表示するログメッセージにのみ影響します。 ゼロ以外の値の設定は、とりわけパラメータがバイナリ形式で送信される際に多少のオーバーヘッドをもたらします。 テキストへの変換が必要になるからです。 -《機械翻訳》«This setting only affects log messages printed as a result of , , and related settings. Non-zero values of this setting add some overhead, particularly if parameters are sent in binary form, since then conversion to text is required.» +《機械翻訳》この設定は、および関連する設定の結果として印刷されるログメッセージにのみ影響します。 +この設定のゼロ以外の値は、特にパラメータがバイナリフォームで送信される場合、テキストへのオーバーヘッドが必要になるため、いくらかの変換を追加します。 @@ -19107,7 +19132,7 @@ LOG: CleanUpLock: deleting: lock(0xb7acd844) id(24688,24696,0,0,0,1) Only has effect if data checksums are enabled. --> 《マッチ度[63.291139]》が有効の時のみ効果があります。 -《機械翻訳》«Only has effect if data checksums are enabled.» +《機械翻訳》データチェックサムが有効になっている場合のみ有効です。 +《機械翻訳》ロールパスワードがpostgresql.confカウントまたはファイルで指定されたの値とは異なる反復サーバコマンドラインで作成された場合、認証されていないユーザは、コネクションの試行に対するサーバの応答の不一致を観察することでロールの存在を識別できます。 +これが問題となる場合は、すべてのロールパスワードがpostgresql.confファイルまたはサーバコマンドラインで指定された値に設定されたで作成された保証を確認します。 +scram_iterations scram_iterations + + @@ -2326,25 +2344,31 @@ TLSバージョン1.3を使用する接続についてはTLS though, and many supported group names and aliases are omitted. --> -最も一般的な曲線のOpenSSL名は、prime256v1(NIST P-256)、secp384r1(NIST P-384)、およびsecp521r1(NIST P-521)です。 +《マッチ度[82.232346]》最も一般的な曲線のOpenSSL名は、prime256v1(NIST P-256)、secp384r1(NIST P-384)、およびsecp521r1(NIST P-521)です。 openssl ecparam -list_curvesコマンドを使用すると、使用可能なグループの不完全なリストを表示することができます。 ただし、すべてがTLSで使用できるわけではなく、サポートされているグループ名と別名の多くは省略されています。 +《機械翻訳》最も一般的なグループのOpenSSL名は、prime256v1 NIST P-256、secp384r1 NIST P-384、secp521r1 NIST P-521です。 +使用可能なグループの不完全なリストは、コマンドOpenSSL ecparam -list_curvesで表示できます。 +すべてのグループがTLSで使用できるわけではありませんが、サポートされているSSL名とエイリアスの多くは省略されています。 diff --git a/doc/src/sgml/config1.sgml b/doc/src/sgml/config1.sgml index 724c4f69017..3ab97796166 100644 --- a/doc/src/sgml/config1.sgml +++ b/doc/src/sgml/config1.sgml @@ -679,15 +679,23 @@ WALの更新をディスクへ強制するのに使用される方法です。 was compiled with ) and zstd (if PostgreSQL was compiled with ). + The value on is a historical spelling of pglz. The default value is off. Only superusers and users with the appropriate SET privilege can change this setting. --> -このパラメータは、指定された圧縮方式を使用したWALの圧縮を有効にします。 +《マッチ度[77.751756]》このパラメータは、指定された圧縮方式を使用したWALの圧縮を有効にします。 有効にすると、PostgreSQLサーバはWALに書き込まれる全ページイメージを圧縮します(例えば、がオンの時やベースバックアップ中です)。 圧縮されたページイメージはWAL再生中に伸長されます。 サポートされている方法はpglzlz4PostgreSQLでコンパイルされた場合)およびzstdPostgreSQLでコンパイルされた場合)です。 デフォルト値はoffです。 +スーパーユーザと適切なSET権限を持つユーザのみがこの設定を変更できます。 +《機械翻訳》このパラメータは、指定された圧縮メソッドを使用したWALの圧縮を有効にします。 +有効になっている場合、PostgreSQLサーバはWALに書き込まれたフルページイメージを圧縮しますがオンの場合、ベースバックアップ中など。 +圧縮されたページイメージはWALリプレイ中に解凍されます。 +サポートされている方法は、pglz,lz4PostgreSQLでコンパイルされた場合とzstdPostgreSQLでコンパイルされた場合です。 +値onpglzの歴史的な綴りです。 +デフォルト値はoffです。 スーパーユーザと適切なSET権限を持つユーザのみがこの設定を変更できます。 @@ -2388,6 +2396,90 @@ WAL要約は、先行するバックアップと新しいバックアップの + + output_plugin_libraries (string) + + output_plugin_libraries configuration parameter + + + + + +《機械翻訳》にインストールされているライブラリのうち、トラステッドクライアントがロジカルのアウトプットプラグインとして使用するためにレプリケーションでもあるライブラリを一覧表示します。 +他のライブラリに対するロジカルデコーディングまたはレプリケーションの要求は拒否されます。 +すべてのユーザはこの制限のサブジェクトです。 +デフォルトは'pgoutput, test_decoding'です。 +これらは標準PostgreSQLディストリビューションに含まれている2つのロジカルアウトプットプラグインです。 + + + +《機械翻訳》フォーマットは、カンマで区切られたライブラリ名のリストです。 +各名前はLOADコマンドと同様に解釈されます(ただし、ロジカルデコーディングクライアントは、ケースやパスの構造に違いがなく、リスト内のエントリと正確に一致するプラグイン名前を指定する必要があります)。 +エントリ間の空白は無視されます。 +名前内でincludeの空白またはカンマが必要な場合は、ライブラリ名前を二重引用符で囲みます。 + + + +《機械翻訳》このサーバに追加されたライブラリが保証にロードされる際に、非スーパーユーザに意図せず追加の権限を与えないようにすることは、リストの管理者の責任です。 +サーバ + + + + +《機械翻訳》output_plugin_libraries更新を持たないバージョンからサーバをパラメータする場合、次の問い合わせは、すべての永続論理レプリケーションスロットで必要とされるプラグインのリストをヘルプコンストラクトすることができます。 + +SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL; + + +《機械翻訳》安全前調整のためにリストを注意深くレビューしてくださいoutput_plugin_libraries。 + + + +《機械翻訳》上記の問い合わせでは、過去に一部のディスプレイのレプリケーションスロットに正常に追加されたプラグインのみをポイントで使用できます。 +新たに拒否されたリクエストは、次のようなメッセージでログに表示されます。 + +ERROR: library "..." may not be used as an output plugin +DETAIL: The configuration parameter "output_plugin_libraries" (currently 'pgoutput, test_decoding') does not name this library as a trusted output plugin. +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. + + + + + + wal_keep_size (integer) diff --git a/doc/src/sgml/config2.sgml b/doc/src/sgml/config2.sgml index 63937b1adf7..da07cee7149 100644 --- a/doc/src/sgml/config2.sgml +++ b/doc/src/sgml/config2.sgml @@ -2348,13 +2348,16 @@ log_line_prefix = '%m [%p] %q%u@%d/%a ' -この設定は、および関連設定の結果を表示するログメッセージにのみ影響します。 +《マッチ度[82.594937]》この設定は、および関連設定の結果を表示するログメッセージにのみ影響します。 ゼロ以外の値の設定は、とりわけパラメータがバイナリ形式で送信される際に多少のオーバーヘッドをもたらします。 テキストへの変換が必要になるからです。 +《機械翻訳》この設定は、および関連する設定の結果として印刷されるログメッセージにのみ影響します。 +この設定のゼロ以外の値は、特にパラメータがバイナリフォームで送信される場合、テキストへのオーバーヘッドが必要になるため、いくらかの変換を追加します。 diff --git a/doc/src/sgml/config3.sgml b/doc/src/sgml/config3.sgml index 23bbfa76375..d988d887fa8 100644 --- a/doc/src/sgml/config3.sgml +++ b/doc/src/sgml/config3.sgml @@ -3992,9 +3992,10 @@ LOG: CleanUpLock: deleting: lock(0xb7acd844) id(24688,24696,0,0,0,1) -が有効の時のみ効果があります。 +《マッチ度[63.291139]》が有効の時のみ効果があります。 +《機械翻訳》データチェックサムが有効になっている場合のみ有効です。 《マッチ度[75.218659]》check_primary_key()およびcheck_foreign_key()は、外部キー制約を検査するために使用されます。 (当然ながら、この機能はかなり前に組み込みの外部キー機能に取って代わりました。しかし例としてはまだ有用です。) -《機械翻訳》«check_primary_key() and check_foreign_key() are used to check foreign key constraints. (This functionality is long since superseded by the built-in foreign key mechanism, of course, but the module is still useful as an example. This module will be removed in PostgreSQL 20.)» +《機械翻訳》check_primary_key()check_foreign_key()はチェック外部キー制約に使用されます。 +(この機能は、もちろん組み込みの外部キーメカニズムに取って代わられてからかなり経っていますが、このモジュールは例として今でも有用です。 +このモジュールはPostgreSQL20.で削除されます。 @@ -65,7 +67,7 @@ secure schema usage pattern and data types where the equality operator is named =. --> -《機械翻訳》«refint requires a secure schema usage pattern and data types where the equality operator is named =.» +《機械翻訳》refintには、セキュアスキーマ使用パターンタイプと、データが記名的である等価演算子タイプが必要です=. @@ -97,7 +99,10 @@ column name arguments should not be double quoted. See the following mock example of proper use of check_primary_key(): --> -《機械翻訳》«The referenced table name and column name arguments to check_primary_key() are copied as-is into internally generated SQL statements and therefore must be double-quoted by the user as necessary in the CREATE TRIGGER command. See for more information about quoting SQL identifiers. Conversely, the referencing table column name arguments should not be double quoted. See the following mock example of proper use of check_primary_key():» +《機械翻訳》check_primary_key()被参照テーブル名前とカラム名前の引数は、内部で生成されたSQL文にそのままコピーされるため、CREATE TRIGGERコマンドで必要に応じてユーザで二重引用符で囲む必要があります。 +SQL識別子の引用符付けの詳細は、を参照してください。 +逆に、参照テーブル列名前の引数は二重引用符で囲む必要はありません。 +check_primary_key():の正しい使用方法については、次の模擬例を参照してください。 CREATE TRIGGER mytrigger AFTER INSERT OR UPDATE ON referencing_table @@ -145,7 +150,11 @@ check_primary_key ( table column name arguments should not be double quoted. See the following mock example of proper use of check_foreign_key(): --> -《機械翻訳》«The referencing table name and column name arguments to check_foreign_key() are copied as-is into internally generated SQL statements and therefore must be double-quoted by the user as necessary in the CREATE TRIGGER command. See for more information about quoting SQL identifiers. Conversely, the referenced table column name arguments should not be double quoted. See the following mock example of proper use of check_foreign_key():» +《機械翻訳》check_foreign_key()参照テーブル名前とカラム名前の引数は、内部で生成されたSQL文にそのままコピーされるため、CREATE TRIGGERコマンドで必要に応じてユーザで二重引用符で囲む必要があります。 +SQL識別子の引用符付けの詳細は、を参照してください。 +逆に、被参照テーブル列名前の引数は二重引用符で囲む必要はありません。 +check_foreign_key():の正しい使用方法については、次の模擬例を参照してください。 + CREATE TRIGGER mytrigger AFTER DELETE OR UPDATE ON referenced_table diff --git a/doc/src/sgml/dblink.sgml b/doc/src/sgml/dblink.sgml index e2337064b3b..f1a2d971ae1 100644 --- a/doc/src/sgml/dblink.sgml +++ b/doc/src/sgml/dblink.sgml @@ -236,7 +236,12 @@ dblink_connect(text connname, text connstr) returns text SCRAMパススルー認証では、dblinkはプレーンテキストユーザパスワードの代わりにSCRAMハッシュ化されたシークレットを使用してリモートサーバに接続します。 これにより、プレーンテキストユーザパスワードがPostgreSQLシステムカタログに格納されるのを回避します。 詳細と制限については、postgres_fdwの相当するuse_scram_passthroughオプションの文書を参照してください。 -《機械翻訳》«The foreign-data wrapper dblink_fdw has an additional Boolean option use_scram_passthrough that controls whether dblink will use the SCRAM pass-through authentication to connect to the remote database. It can be specified for a foreign server or a user mapping. A user mapping setting overrides the foreign server setting. With SCRAM pass-through authentication, dblink uses SCRAM-hashed secrets instead of plain-text user passwords to connect to the remote server. This avoids storing plain-text user passwords in PostgreSQL system catalogs. See the documentation of the equivalent use_scram_passthrough option of postgres_fdw for further details and restrictions.» +《機械翻訳》外部データラッパdblink_fdwには、追加のブーリアンオプションがありますuse_scram_passthroughdblinkがSCRAMパススルー認証を使用してリモートデータベースに接続するかどうかを制御します。 +これは外部サーバまたはユーザマッピングに対して指定できます。 +ユーザマッピング設定は外部サーバ設定を上書きします。 +SCRAMパススルー認証では、dblinkSCRAMでハッシュされたシークレット代わりプレーンテキストユーザパスワードを使用してリモートサーバに接続します。 +これにより、プレーンテキストユーザパスワードがPostgreSQLシステムカタログに格納されるのを回避できます。 +詳細と制限については、POSTGRES_fdwの同等のuse_scram_passthroughオプションの文書を参照してください。 diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 5acae78469f..14dd6a88f52 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -3201,7 +3201,10 @@ REVOKE ALL ON accounts FROM PUBLIC; a cast function, and such functions will be called with the privileges of the table owner. --> -《機械翻訳》«Allows creation of a foreign key constraint referencing a table, or specific column(s) of a table. Great care should be taken when granting this privilege, since a user who creates a foreign key can arrange for enforcement of that foreign key to call an arbitrary function, such as a cast function, and such functions will be called with the privileges of the table owner.» +《機械翻訳》テーブルの外部キー制約参照、またはテーブルの特定のカラムを作成することができます。 +この権限を付与する際には十分な注意が必要です。 +ユーザを作成した外部キーは、キャスト関数のような任意の関数を呼び出しに強制するように調整することができ、そのような関数はテーブル所有者の権限で呼び出されるからです。 +外部キー @@ -3215,7 +3218,8 @@ REVOKE ALL ON accounts FROM PUBLIC; taken when granting this privilege, since any triggers added to a table or view will be executed with the privileges of users who modify it. --> -《機械翻訳》«Allows creation of a trigger on a table, view, etc. Great care should be taken when granting this privilege, since any triggers added to a table or view will be executed with the privileges of users who modify it.» +《機械翻訳》テーブルやビューなどにトリガを作成することを許可します。 +テーブルや権限に追加されたトリガはそれを変更するユーザの権限で実行されるため、このビューを付与する際には十分な注意が必要です。 diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml index 7a527cccb2f..5e7f4724f5d 100644 --- a/doc/src/sgml/ecpg.sgml +++ b/doc/src/sgml/ecpg.sgml @@ -10347,7 +10347,11 @@ GET DESCRIPTOR descriptor_name VALU 行数が1つの例です。 列番号を追加のパラメータとして必要とする2番目の構文では特定の列に関する情報を取り出します。 例えば、列名と列の実際の値です。 -《機械翻訳》«This command has two forms: The first form retrieves descriptor header item, which applies to the result set in its entirety. One example is the row count. The second form, which requires the column number as additional parameter, retrieves information about a particular column. Examples are the column name and the actual column value.» +《機械翻訳》このコマンドには2つの形式があります。 +最初のフォームは、結果セット全体に適用されるディスクリプタヘッダアイテムを取得します。 +1つ目の例は行カウントです。 +2つ目のフォームは、追加パラメータとしてカラム番号を必要とし、特定のカラムに関する情報を取得します。 +たとえば、カラム名前や実際のカラム値などです。 diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 234d35f3439..7bc1e605b32 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -6622,7 +6622,7 @@ cast(-1234 as bytea) \xfffffb2e (string length is preserved) --> 《マッチ度[65.957447]》ビット単位の左シフト(文字列長は保存されます) -《機械翻訳》«Bitwise shift left (string length is preserved)» +《機械翻訳》ビット単位のシフトレフト(文字列長さは保持されます)。 B'10001' << 3 @@ -6641,7 +6641,7 @@ cast(-1234 as bytea) \xfffffb2e (string length is preserved) --> 《マッチ度[66.666667]》ビット単位の右シフト(文字列長は保存されます) -《機械翻訳》«Bitwise shift right (string length is preserved)» +《機械翻訳》ビット単位のシフト右(文字列長さは保持されます)。 B'10001' >> 2 @@ -18987,7 +18987,13 @@ OIDで指定したパーサが認識できるトークンの型を記述する An error is raised if the resulting timestamp is outside this range. --> -《機械翻訳》«Generates a version 7 (time-ordered) UUID. The timestamp is computed using UNIX timestamp with millisecond precision + sub-millisecond timestamp + random. The optional parameter shift will shift the computed timestamp by the given interval. Infinite interval values are not accepted. The shifted timestamp must fall within the range supported by UUID version 7's 48-bit millisecond timestamp field: from 1970-01-01 00:00:00 UTC to approximately year 10889. An error is raised if the resulting timestamp is outside this range.» +《機械翻訳》バージョン7 shift時間順UUIDを生成します。 +タイムスタンプは、UNIXタイムスタンプを使用して、ミリ秒精度+サブ-ミリ秒タイムスタンプ+ランダムで計算されます。 +オプショナルパラメータは、指定されたインターバルによって計算されたタイムスタンプをシフトします。 +無限インターバルの値は受け入れられません。 +シフトされたタイムスタンプは、UUIDバージョン7の48-ビット(1970-01-01 00:00:00 UTCから約10889年まで)でサポートされるレンジの範囲内にある必要があります。 +生成されたがこのの外にある場合は、が発生します。 +レンジミリ秒エラーフィールドタイムスタンプタイムスタンプ uuidv7() diff --git a/doc/src/sgml/func1.sgml b/doc/src/sgml/func1.sgml index d81dca39782..7f51c4feee4 100644 --- a/doc/src/sgml/func1.sgml +++ b/doc/src/sgml/func1.sgml @@ -6552,7 +6552,8 @@ cast(-1234 as bytea) \xfffffb2e Bitwise shift left (string length is preserved) --> -ビット単位の左シフト(文字列長は保存されます) +《マッチ度[65.957447]》ビット単位の左シフト(文字列長は保存されます) +《機械翻訳》ビット単位のシフトレフト(文字列長さは保持されます)。 B'10001' << 3 @@ -6570,7 +6571,8 @@ cast(-1234 as bytea) \xfffffb2e Bitwise shift right (string length is preserved) --> -ビット単位の右シフト(文字列長は保存されます) +《マッチ度[66.666667]》ビット単位の右シフト(文字列長は保存されます) +《機械翻訳》ビット単位のシフト右(文字列長さは保持されます)。 B'10001' >> 2 diff --git a/doc/src/sgml/func2.sgml b/doc/src/sgml/func2.sgml index be322f847bb..767b92b7367 100644 --- a/doc/src/sgml/func2.sgml +++ b/doc/src/sgml/func2.sgml @@ -8502,10 +8502,20 @@ OIDで指定したパーサが認識できるトークンの型を記述する sub-millisecond timestamp + random. The optional parameter shift will shift the computed timestamp by the given interval. ---> -バージョン7(時間順)のUUIDを生成します。 -タイムスタンプは、ミリ秒精度のUNIXタイムスタンプ+サブミリ秒のタイムスタンプ+ランダム値を使用して計算されます。 -オプションのパラメータshiftは、計算されたタイムスタンプを指定されたintervalだけシフトします。 + Infinite interval values are not accepted. + The shifted timestamp must fall within the range supported by + UUID version 7's 48-bit millisecond timestamp field: from + 1970-01-01 00:00:00 UTC to approximately year 10889. + An error is raised if the resulting timestamp is outside this + range. +--> +《機械翻訳》バージョン7 shift時間順UUIDを生成します。 +タイムスタンプは、UNIXタイムスタンプを使用して、ミリ秒精度+サブ-ミリ秒タイムスタンプ+ランダムで計算されます。 +オプショナルパラメータは、指定されたインターバルによって計算されたタイムスタンプをシフトします。 +無限インターバルの値は受け入れられません。 +シフトされたタイムスタンプは、UUIDバージョン7の48-ビット(1970-01-01 00:00:00 UTCから約10889年まで)でサポートされるレンジの範囲内にある必要があります。 +生成されたがこのの外にある場合は、が発生します。 +レンジミリ秒エラーフィールドタイムスタンプタイムスタンプ uuidv7() diff --git a/doc/src/sgml/func4.sgml b/doc/src/sgml/func4.sgml index cc921091af0..eee73c76028 100644 --- a/doc/src/sgml/func4.sgml +++ b/doc/src/sgml/func4.sgml @@ -2792,23 +2792,6 @@ PUBLIC仮想ロールは実在するロールのメンバには決してなれ t - - - - aclitem[] ~ aclitem - boolean - - - -これは@>の廃止予定の別名です。 - - - '{calvin=r*w/hobbes,hobbes=r*w*/postgres}'::aclitem[] ~ 'calvin=r*/hobbes'::aclitem - t - - diff --git a/doc/src/sgml/libpq2.sgml b/doc/src/sgml/libpq2.sgml index 962cf239a50..3002ba0399b 100644 --- a/doc/src/sgml/libpq2.sgml +++ b/doc/src/sgml/libpq2.sgml @@ -1006,11 +1006,11 @@ int PQrequestCancel(PGconn *conn); with binary transmission of parameters and results substitutes for a fast-path function call. --> -《機械翻訳》このインタフェースは安全ではないため、使用しないでください。 +このインタフェースは安全ではないため、使用しないでください。 result_is_int0に設定されている場合、PQfnは、要求されたバイト数に対してデータに十分なスペースがあるかどうかにかかわらず、result_bufの末尾を超えてバッファを書き込むことができます。 さらに、これは廃止されました。 -関数呼び出しを定義するためにプリペアド文を設定することによって、同様のパフォーマンスおよびより大きな機能を達成できます。 -次に、パラメータおよび結果のバイナリ伝送を伴うステートメントを実行すると、fast-パス関数呼び出しの代わりになります。 +関数呼び出しを定義するためにプリペアド文を設定することによって、同等のパフォーマンスおよびより多くの機能を得られるからです。 +そして、パラメータおよび結果のバイナリ伝送を伴うステートメントを実行すると、近道関数呼び出しの代わりになります。 diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index c6a2741c23b..8c0f07a4dec 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -3276,7 +3276,8 @@ WAL送信プロセスはWALのロジカルデコーディング(options=-coutput_plugin_libraries=... in the connection string. --> -《機械翻訳》«The name of the output plugin used by the replication connection must be included in the server's . (For subscriptions, the plugin name that is used is pgoutput.) Superusers may modify the trusted list per-connection, by including options=-coutput_plugin_libraries=... in the connection string.» +《機械翻訳》レプリケーション名前で使用される出力用プラグインのコネクションは、サーバのに含まれている必要があります(サブスクリプションの場合、使用されるプラグイン名前はpgoutput.です。 +スーパーユーザは、options=-coutput_plugin_libraries=...をコネクショントラステッドに含めることで、コネクションごとに文字列リストを変更できます。 @@ -3618,7 +3619,9 @@ WAL送信プロセスはWALのロジカルデコーディング(; see that parameter's documentation for safety information. --> -《機械翻訳》«The output plugins referenced by the slots in the old cluster must be installed in the new PostgreSQL executable directory. They must also be included in the new cluster's ; see that parameter's documentation for safety information.» +《機械翻訳》古い被参照のスロットにあるクラスタの出力プラグインは、新しいPostgreSQLの実行可能ディレクトリにインストールする必要があります。 +また、新しいクラスタのにも含める必要があります。 +安全に関する情報については、そのパラメータの文書を参照してください。 diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 95833edbbb2..1c32339117e 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -1076,7 +1076,9 @@ HINT: Execute a database-wide VACUUM in that database. 《マッチ度[82.068966]》古いプリペアドトランザクションを解決します。 pg_prepared_xactsのage(transactionid)が大きい行を確認して見つけることができます。 このようなトランザクションはコミットまたはロールバックされるべきです。 -《機械翻訳》«Resolve old prepared transactions. You can find these by checking pg_prepared_xacts for rows where age(transactionid) is large. Such transactions should be committed or rolled back.» +《機械翻訳》古い準備されたトランザクションを解決します。 +age(transactionid)がプリペアドである行に対してpg_ラージ_xactsをチェックすることで、これらを見つけることができます。 +このようなトランザクションはコミットもしくはロールバックされるべきです。 -《機械翻訳》«Unlike transaction ID wraparound, replication slots do not directly hold back multixact cleanup. Dropping stale replication slots is therefore not usually relevant to resolving multixact ID wraparound problems.» +《機械翻訳》トランザクションIDの周回とは異なり、レプリケーションスロットはmultixactクリーンアップを直接抑制しません。 +したがって、古いレプリケーションスロットを削除することは、通常、マルチトランザクションIDの周回問題の解決には関係ありません。 《マッチ度[78.723404]》shutdown_cbコールバックは、接続に関連付けられたバックエンドプロセスが終了するときに実行されます。 検証器モジュールにメモリを割り当てられた状態がある場合、このコールバックはリソースリークを回避するためにフリーする必要があります。 -《機械翻訳》«The shutdown_cb callback is executed when the server backend has finished validating tokens for the connection. If the validator module has any allocated state, this callback should free it to avoid resource leaks.» +《機械翻訳》shutdown_cbコールバックは、サーババックエンドがコネクションのトークンの検証を完了したときに実行されます。 +バリデータモジュールに割り当てられた状態がある場合、このコールバックはリソースリークを回避するためにそれをフリーする必要があります。 typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state); diff --git a/doc/src/sgml/pgcrypto.sgml b/doc/src/sgml/pgcrypto.sgml index 25b778bf58f..b6499aed33b 100644 --- a/doc/src/sgml/pgcrypto.sgml +++ b/doc/src/sgml/pgcrypto.sgml @@ -1542,7 +1542,10 @@ Applies to: pgp_sym_encrypt, pgp_pub_encrypt enabled, so there is no guarantee that the decrypted plaintext actually originated from a holder of the key. --> -《機械翻訳》«Dangerous! Instructs pgcrypto to use an incorrect decryption algorithm matching the historical behavior prior to the fix for CVE-2026-14663, by completely ignoring failures from the OpenSSL cipher in use. This is intended only for users who need to recover incorrectly-encrypted messages created when the cipher-algo was unavailable under the OpenSSL configuration in use. Such faulty messages do not require the correct decryption key when ignore-cipher-failure is enabled, so there is no guarantee that the decrypted plaintext actually originated from a holder of the key.» +《機械翻訳》Dangerous!使用中のOpenSSL暗号の失敗を完全に無視することにより、pgcryptoに不正な復号化アルゴリズムマッチングを使用するように指示します。 +これは、使用中のOpenSSL設定でcipher-algoが利用できなかった場合に作成された、誤って暗号化されたメッセージを回復する必要があるユーザのみを対象としています。 +このような不正なメッセージは、ignore-cipher-failureが有効になっている場合は正しい復号化キーを必要としないため、復号化された平文が実際にキーのホルダーから発信されたものであるという保証はありません。 +14663 2026 -《機械翻訳》«Contrast the case of a message which was correctly encrypted, but the cipher that produced it is unavailable under the current OpenSSL configuration. Recovering such plaintext via pgcrypto requires making the actual cipher available to OpenSSL by, for example, enabling the appropriate provider. ignore-cipher-failure is not necessary or helpful for that scenario. If decryption of a correctly encrypted message with this option happens to pass PGP integrity checks, that result is coincidental and does not make the recovered plaintext trustworthy.» +《機械翻訳》正しく暗号化されたメッセージのケースと、それを生成した暗号が現在OpenSSL設定では利用できないことを対比させてください。 +このような平文をpgcryptoを介して回復するには、例の場合、適切なプロバイダを有効にすることによって、実際の暗号をOpenSSLで利用できるようにする必要があります。 +ignore-cipher-failureはそのシナリオにとって必要でも有用でもありません。 +このオプションを使用して正しく暗号化されたメッセージの復号化がたまたまPGP整合性チェックに合格した場合、その結果は偶然の一致であり、回復された平文をmakeすることはできません。 Values: 0, 1 @@ -1983,7 +1989,7 @@ fips_mode() returns boolean pgp_pub_encrypt() do not use built in crypto so they are not affected. --> -《機械翻訳》«pgp_sym_encrypt() and pgp_pub_encrypt() do not use built in crypto so they are not affected.» +《機械翻訳》pgp_sym_encrypt()およびpgp_pub_encrypt()は、組み込み暗号を使用しないでください。 diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index ccdbdbd1a7d..1884a791f78 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -1134,7 +1134,11 @@ check制約でそのような一貫しない動作があると、問い合わせ 《マッチ度[68.297456]》このオプションは、postgres_fdwがSCRAMパススルー認証を使用して外部サーバに接続するかどうかを制御します。 SCRAMパススルー認証では、postgres_fdwはプレーンテキストユーザパスワードの代わりにSCRAMハッシュ化されたシークレットを使用してリモートサーバに接続します。 これにより、プレーンテキストユーザパスワードがPostgreSQLシステムカタログに格納されるのを回避します。 -《機械翻訳》«This option controls whether postgres_fdw will use the SCRAM pass-through authentication to connect to the foreign server. It can be specified for a foreign server or a user mapping. A user mapping setting overrides the foreign server setting. With SCRAM pass-through authentication, postgres_fdw uses SCRAM-hashed secrets instead of plain-text user passwords to connect to the remote server. This avoids storing plain-text user passwords in PostgreSQL system catalogs.» +《機械翻訳》このオプションは、postgres_fdwがSCRAMパススルー認証を使用して外部サーバに接続するかどうかを制御します。 +これは外部サーバまたはユーザマッピングに対して指定できます。 +ユーザマッピング設定は外部サーバ設定より優先されます。 +SCRAMパススルー認証では、postgres_fdw SCRAMでハッシュされたシークレット代わりplain-テキストユーザパスワードを使用してリモートサーバに接続します。 +これにより、plain-テキストユーザパスワードがPostgreSQLシステムカタログに保存されるのを回避できます。 diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 18cdd0d88cf..6310bcae59e 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -1474,7 +1474,15 @@ fillfactor、TOAST、およびautovacuumのストレージパラメータおよ また、同等のインデックスが既にある場合には、そのインデックスが、ALTER INDEX ATTACH PARTITIONが実行された場合と同様に、対象テーブルのインデックスに付加されます。 既存のテーブルが外部テーブルの場合、今のところ対象テーブルにUNIQUEインデックスがあるときにはテーブルを対象テーブルのパーティションとして追加することはできない点に注意してください(も参照してください)。 対象テーブルにある各ユーザ定義の行レベルのトリガに対しては、対応するものが付加されるテーブルに作られます。 -《機械翻訳》«This form attaches an existing table (which might itself be partitioned) as a partition of the target table. The table can be attached as a partition for specific values using FOR VALUES or as a default partition by using DEFAULT. For each index in the target table, if a valid equivalent index already exists in the partition, it will be attached to the target table's index, as if ALTER INDEX ATTACH PARTITION had been executed; otherwise, a new corresponding index will be created. Invalid indexes on the partition are skipped. Note that if the existing table is a foreign table, it is currently not allowed to attach the table as a partition of the target table if there are UNIQUE indexes on the target table. (See also .) For each user-defined row-level trigger that exists in the target table, a corresponding one is created in the attached table.» +《機械翻訳》このフォームは、既存のテーブル(自分自身がパーティション化されている場合もあります)をターゲットテーブルのパーティションとしてアタッチします。 +テーブルは、を使用して特定の値のパーティションとしてFOR VALUESまたはDEFAULTを使用してデフォルトパーティションとしてアタッチできます。 +ターゲットテーブルのインデックスごとに、同等の有効なインデックスがパーティションにすでに存在する場合は、ALTERインデックスアタッチパーティションが実行された場合と同様にターゲットテーブルのインデックスにアタッチされます。 +それ以外の場合は、対応するインデックスが新規作成されます。 +パーティションの無効なインデックスはスキップされたです。 +ノートでは、既存のがである場合、UNIQUEのインデックスが存在する場合には、そのをのとして挿入することは現在許可されていません。 +(も参照してください。) +内に存在する定義された-✔ごとに、対応するものがアタッチされたに作成されます。 +テーブルアタッチ行テーブル外部テーブルパーティションターゲットテーブルテーブルターゲットテーブルターゲットテーブルレベルトリガユーザ diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index 3d425e407d6..def44c7774b 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -685,7 +685,8 @@ WHERE condition functions). --> 《マッチ度[86.379928]》今のところ、WHERE式の中での副問い合わせは認められていませんし、評価はCOPY自身により行われた変更を見ることはありません(これは、式がVOLATILE関数の呼び出しを含む場合に問題になります)。 -《機械翻訳》«Currently, subqueries and generated columns are not allowed in WHERE expressions, and the evaluation does not see any changes made by the COPY itself (this matters when the expression contains calls to VOLATILE functions).» +《機械翻訳》現在、サブ問い合わせと生成された列はWHERE式では許可されず、評価ではCOPY自分自身によって行われた変更は確認されません。 +これは、式包含がVOLATILE関数を呼び出すときに重要です。 diff --git a/doc/src/sgml/ref/create_type.sgml b/doc/src/sgml/ref/create_type.sgml index 20e1885e706..91b47870ae5 100644 --- a/doc/src/sgml/ref/create_type.sgml +++ b/doc/src/sgml/ref/create_type.sgml @@ -276,7 +276,7 @@ CREATE TYPE name privilege on the subtype. --> 《マッチ度[72.727273]》複合型を作成するためには、すべての属性型に対してUSAGE権限を持たなければなりません。 -《機械翻訳》«To be able to create a range type, you must have USAGE privilege on the subtype.» +《機械翻訳》範囲型を作成するには、派生元型にUSAGE権限が必要です。 diff --git a/doc/src/sgml/ref/drop_subscription.sgml b/doc/src/sgml/ref/drop_subscription.sgml index 8ce8a8bb03a..8368162a36f 100644 --- a/doc/src/sgml/ref/drop_subscription.sgml +++ b/doc/src/sgml/ref/drop_subscription.sgml @@ -155,7 +155,16 @@ DROP SUBSCRIPTION [ IF EXISTS ] nameも参照してください。 -《機械翻訳》«When dropping a subscription that is associated with a replication slot on the remote host (the normal state), DROP SUBSCRIPTION will connect to the remote host and try to drop the replication slot (and any remaining table synchronization slots) as part of its operation. This is necessary so that the resources allocated for the subscription on the remote host are released. If this fails, either because the remote host is not reachable or because the remote replication slot cannot be dropped or does not exist or never existed, the DROP SUBSCRIPTION command will fail. To proceed in this situation, first disable the subscription by executing ALTER SUBSCRIPTION ... DISABLE, and then disassociate it from the replication slot by executing ALTER SUBSCRIPTION ... SET (slot_name = NONE). After that, DROP SUBSCRIPTION will not attempt to drop the subscription's own replication slot. It may still connect to the publisher to drop internally-created table synchronization slots if some table synchronization is left unfinished; if the publisher is unreachable, those slots (and the main slot, if it still exists) must be dropped manually. Otherwise it/they will continue to reserve WAL and might eventually cause the disk to fill up. See also .» +《機械翻訳》リモートホストのサブスクリプションに関連付けられたレプリケーションスロットを削除する場合(通常の状態)、DROP SUBSCRIPTIONはリモートホストとトライに接続して、レプリケーションスロット(および残りのテーブル同期化スロット)をオペレーションのパートとして削除します。 +これは、リモートホストのサブスクリプションに割り当てられたリソースが解放されるようにするために必要です。 +リモートホストが到達不能であるか、リモートレプリケーションスロットが削除できないか、存在しないか、存在しなかったために、これが失敗した場合、DROP SUBSCRIPTIONコマンドは失敗します。 +このシチュエーションで処理を続行するには、まずALTER SUBSCRIPTION ... DISABLEを実行してサブスクリプションを無効にし、ALTER SUBSCRIPTION ... SET (slot_name = NONE)を実行してレプリケーションスロットとの関連付けを解除します。 +その後、DROP SUBSCRIPTIONはサブスクリプション自身のレプリケーションスロットを削除しようとしません。 +テーブルの同期化が完了していない場合は、パブリッシャーに接続して、内部で作成されたテーブル同期化スロットを削除することができます。 +パブリッシャーが到達不能な場合は、それらのスロット(およびがまだ存在する場合は)を手動で削除する必要があります。 +それ以外の場合は、WALの予約が継続され、最終的にがいっぱいになる可能性があります。 +も参照してください。 +ディスクメインスロット diff --git a/doc/src/sgml/ref/pg_recvlogical.sgml b/doc/src/sgml/ref/pg_recvlogical.sgml index daf7910d3f8..66513fdf0cb 100644 --- a/doc/src/sgml/ref/pg_recvlogical.sgml +++ b/doc/src/sgml/ref/pg_recvlogical.sgml @@ -708,7 +708,7 @@ LSNがlsnと正確に一致するレコードがあ cluster. --> 《マッチ度[85.906040]》pg_recvlogicalは、ソースクラスタでグループパーミッションが有効である場合、受け取ったWALファイルのグループパーミッションを維持します。 -《機械翻訳》«pg_recvlogical will preserve group permissions on the output files if group permissions are enabled on the source cluster.» +《機械翻訳》pg_recvlogicalグループクラスタでグループ許可が有効になっている場合、は出力されたファイルにソース許可を保持します。 diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 09e12fc8319..14581e16356 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -3962,7 +3962,11 @@ SELECT コマンド名にxが付与された場合は、拡張モードで結果が表示されます。 コマンド名に+が付与された場合は、データベースのサイズ、デフォルトのテーブル空間、および説明も表示します。 (サイズ情報は現在のユーザが接続可能なデータベースでのみ表示されます。) -《機械翻訳》«List the databases in the server and show their names, owners, character set encodings, and access privileges. If pattern is specified, only databases whose names match the pattern are listed. If x is appended to the command name, the results are displayed in expanded mode. If + is appended to the command name, database sizes, default tablespaces, and descriptions are also displayed. Size information is available for databases on which the current user has CONNECT privilege, or if the current user is a superuser or has privileges of the pg_read_all_stats role.» +《機械翻訳》サーバ内のデータベースをリストし、その名前、所有者、キャラクタセットのエンコード方式およびアクセスの権限を表示します。 +パターンが指定されている場合は、名前がマッチパターンであるデータベースのみがリストされます。 +xがコマンド名前に追加されている場合は、結果が拡張モードに表示されます。 ++がコマンド名前に追加されている場合は、データベースのサイズ、デフォルトの表領域および説明も表示されます。 +サイズ情報は、現在ユーザが権限であるかCONNECT、現在ユーザがスーパーユーザであるかpg_read_all_statsロールの権限を持つデータベースで使用できます。 @@ -5437,7 +5441,7 @@ SELECT 1 \bind \sendpipeline neither variable interpolation nor backquote expansion are performed. --> 《マッチ度[70.813397]》他のほとんどのメタコマンドと異なり、行の残り部分はすべて\efの引数であると常に解釈され、引数内の変数の置換も逆引用符の展開も行われません。 -《機械翻訳》«Unlike most other meta-commands, the entire remainder of the line is always taken to be the argument of \unrestrict, and neither variable interpolation nor backquote expansion are performed.» +《機械翻訳》他のほとんどのメタコマンドとは異なり、直線の残り全体は常に\unrestrictの引数とみなされ、変数補間も逆引用符拡張も実行されません。 From fb3d45a2ea7e717e7862c32b400873580dd713a3 Mon Sep 17 00:00:00 2001 From: Noboru Saito Date: Fri, 14 Aug 2026 00:39:13 +0900 Subject: [PATCH 250/250] =?UTF-8?q?18.6=E7=BF=BB=E8=A8=B3=E6=BA=96?= =?UTF-8?q?=E5=82=995=20release-18.sgml=E3=81=AE=E6=A9=9F=E6=A2=B0?= =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82NG?= =?UTF-8?q?=E8=AA=9E=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/src/sgml/dblink.sgml | 2 +- doc/src/sgml/ref/drop_subscription.sgml | 3 +- doc/src/sgml/release-18.sgml | 695 ++++++++++++++---------- 3 files changed, 403 insertions(+), 297 deletions(-) diff --git a/doc/src/sgml/dblink.sgml b/doc/src/sgml/dblink.sgml index f1a2d971ae1..be23020f26d 100644 --- a/doc/src/sgml/dblink.sgml +++ b/doc/src/sgml/dblink.sgml @@ -236,7 +236,7 @@ dblink_connect(text connname, text connstr) returns text SCRAMパススルー認証では、dblinkはプレーンテキストユーザパスワードの代わりにSCRAMハッシュ化されたシークレットを使用してリモートサーバに接続します。 これにより、プレーンテキストユーザパスワードがPostgreSQLシステムカタログに格納されるのを回避します。 詳細と制限については、postgres_fdwの相当するuse_scram_passthroughオプションの文書を参照してください。 -《機械翻訳》外部データラッパdblink_fdwには、追加のブーリアンオプションがありますuse_scram_passthroughdblinkがSCRAMパススルー認証を使用してリモートデータベースに接続するかどうかを制御します。 +《機械翻訳》外部データラッパーdblink_fdwには、追加のブールオプションがありますuse_scram_passthroughdblinkがSCRAMパススルー認証を使用してリモートデータベースに接続するかどうかを制御します。 これは外部サーバまたはユーザマッピングに対して指定できます。 ユーザマッピング設定は外部サーバ設定を上書きします。 SCRAMパススルー認証では、dblinkSCRAMでハッシュされたシークレット代わりプレーンテキストユーザパスワードを使用してリモートサーバに接続します。 diff --git a/doc/src/sgml/ref/drop_subscription.sgml b/doc/src/sgml/ref/drop_subscription.sgml index 8368162a36f..3f1b59a8833 100644 --- a/doc/src/sgml/ref/drop_subscription.sgml +++ b/doc/src/sgml/ref/drop_subscription.sgml @@ -162,9 +162,8 @@ DROP SUBSCRIPTION [ IF EXISTS ] nameDROP SUBSCRIPTIONはサブスクリプション自身のレプリケーションスロットを削除しようとしません。 テーブルの同期化が完了していない場合は、パブリッシャーに接続して、内部で作成されたテーブル同期化スロットを削除することができます。 パブリッシャーが到達不能な場合は、それらのスロット(およびがまだ存在する場合は)を手動で削除する必要があります。 -それ以外の場合は、WALの予約が継続され、最終的にがいっぱいになる可能性があります。 +それ以外の場合は、WALの予約が継続され、最終的にディスクがいっぱいになる可能性があります。 も参照してください。 -ディスクメインスロット diff --git a/doc/src/sgml/release-18.sgml b/doc/src/sgml/release-18.sgml index 8b0123dfe86..4f3edb5cf53 100644 --- a/doc/src/sgml/release-18.sgml +++ b/doc/src/sgml/release-18.sgml @@ -27,7 +27,7 @@ Note: 18.5 was never released, due to a regression discovered post-wrap. --> -《機械翻訳》«Note: 18.5 was never released, due to a regression discovered post-wrap.» +《機械翻訳》ノート:18.5は、ラップ後に発見されたリグレッションのため、リリースされませんでした。 @@ -48,7 +48,7 @@ However, the first three security entries below describe configuration adjustments and data cleanups that you may need to make after updating. --> -《機械翻訳》«However, the first three security entries below describe configuration adjustments and data cleanups that you may need to make after updating.» +《機械翻訳》ただし、以下の最初の3つのセキュリティ項目では、更新後にmakeする必要がある設定調整とデータ清掃について説明します。 @@ -57,7 +57,7 @@ about possibly-corrupt reltuples values for their tables. --> -《機械翻訳》«Also, if you have any GIN indexes, see the changelog entry below about possibly-corrupt reltuples values for their tables.» +《機械翻訳》また、GINインデックスがある場合は、以下のエントリの変更ログで破損の可能性reltuplesテーブルの値について確認してください。 @@ -66,7 +66,8 @@ or contrib/ltree, you may need to reindex indexes made with those extensions; see the relevant entries below. --> -《機械翻訳》«Also, if you use contrib/btree_gist or contrib/ltree, you may need to reindex indexes made with those extensions; see the relevant entries below.» +《機械翻訳》また、contrib/btree_gistまたはcontrib/ltreeを使用する場合は、これらの拡張で作成されたインデックスをインデックス再作成する必要があるかもしれません。 +以下の関連するエントリを参照してください。 @@ -74,7 +75,7 @@ Also, if you are upgrading from a version earlier than 18.2, see . --> -《機械翻訳》«Also, if you are upgrading from a version earlier than 18.2, see .» +《機械翻訳》また、18.2より前のバージョンからアップグレードする場合は、を参照してください。 @@ -108,7 +109,7 @@ Branch: REL_17_STABLE [4fcccea97] 2026-08-10 06:38:18 -0700 new server parameter output_plugin_libraries (Jacob Champion) --> -《機械翻訳》«Restrict logical decoding output plugins to the set specified by a new server parameter output_plugin_libraries » +《機械翻訳》ロジカルデコーディング出力プラグインを、新しいサーバパラメータoutput_plugin_libraries. (Jacob Champion) § § @@ -121,7 +122,8 @@ Branch: REL_17_STABLE [4fcccea97] 2026-08-10 06:38:18 -0700 locking this down without breaking setups that worked before, introduce a whitelist of allowed output plugins. --> -《機械翻訳》«Previously, a replication user could select any loadable library for logical decoding, allowing exploits of various sorts. To allow locking this down without breaking setups that worked before, introduce a whitelist of allowed output plugins.» +《機械翻訳》これまでは、レプリケーションユーザはロジカルデコーディングのロード可能ライブラリをセレクトすることができ、さまざまな種類のエクスプロイトを可能にしていた。 +前で動作していたセットアップを壊すことなくこのダウンをロックできるようにするために、許可されたアウトプットプラグインのホワイトリストを導入した。 @@ -133,7 +135,8 @@ Branch: REL_17_STABLE [4fcccea97] 2026-08-10 06:38:18 -0700 Installations that rely on other output plugins must add them after updating the server, for example --> -《機械翻訳》«By default, only the output plugins shipped as part of PostgreSQL (pgoutput and test_decoding) are included in output_plugin_libraries. Installations that rely on other output plugins must add them after updating the server, for example» +《機械翻訳》デフォルト別では、パートで出荷されたPostgreSQLpgoutputおよびtest_decodingの出力プラグインのみがoutput_plugin_librariesに含まれます。 +他の出力プラグインに依存するインストールでは、例のサーバ更新の後にそれらを追加する必要があります。 output_plugin_libraries = 'pgoutput, test_decoding, my_trusted_decoder' @@ -148,7 +151,8 @@ output_plugin_libraries = 'pgoutput, test_decoding, my_trusted_decoder' Make necessary additions to the new cluster's setting before performing pg_upgrade. --> -《機械翻訳》«Additionally, pg_upgrade --check will fail if the output_plugin_libraries parameter on the new cluster does not permit the plugins of logical replication slots on the old cluster, when migrating from versions 17 and later. Make necessary additions to the new cluster's setting before performing pg_upgrade.» +《機械翻訳》さらに、pg_upgrade--チェックは、バージョン17以降から移行する場合、output_plugin_libraries新しいクラスタのパラメータが古いクラスタの論理レプリケーションスロットのプラグインを許可しない場合に失敗します。 +makeは、新しいクラスタの設定前に必要な追加を実行しますpg_upgrade @@ -158,7 +162,7 @@ output_plugin_libraries = 'pgoutput, test_decoding, my_trusted_decoder' for reporting this problem. (CVE-2026-6471) --> -《機械翻訳》«The PostgreSQL Project thanks Vladimir Tokarev and Yu Kunpeng for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたVladimir TokarevとYu Kunpengに感謝します。 (CVE-2026-6471) @@ -187,7 +191,7 @@ Branch: REL_14_STABLE [e2c48c81f] 2026-08-10 06:38:37 -0700 Fix contrib/pgcrypto's PGP encryption to detect unsupported ciphers (Daniel Gustafsson) --> -《機械翻訳》«Fix contrib/pgcrypto's PGP encryption to detect unsupported ciphers » +《機械翻訳》サポートされていない暗号を検出するために、contrib/pgcryptoのPGP暗号化を修正。 (Daniel Gustafsson) § § @@ -204,7 +208,8 @@ Branch: REL_14_STABLE [e2c48c81f] 2026-08-10 06:38:37 -0700 cipher algorithms (cipher-algo=blowfish/bf, twofish, cast5, or 3des). --> -《機械翻訳》«Previously, if OpenSSL rejected the requested cipher (for example, because it is running in FIPS mode, or the legacy provider hasn't been loaded), pgcrypto failed to notice the failure and simply XOR'd the non-encrypted block with the plaintext, rendering the encryption trivially breakable. This will typically occur with deprecated or non-FIPS cipher algorithms (cipher-algo=blowfish/bf, twofish, cast5, or 3des).» +《機械翻訳》以前は、OpenSSLが要求された暗号を拒否した場合(FIPS例で実行されているか、レガシープロバイダがロードされていないため、モード用、pgcrypto失敗に気づかず、暗号化されていないブロックとプレーンテキストを単純にXORして、暗号化を些細な解読可能なものにしていました。 +これは通常、非推奨またはFIPS以外の暗号アルゴリズム(cipher-algo=blowfish/bf、twofish、キャスト5、または3 des)で発生します。 @@ -219,7 +224,9 @@ Branch: REL_14_STABLE [e2c48c81f] 2026-08-10 06:38:37 -0700 their previous behavior, allowing the faulty encryption wrapper to be stripped off: --> -《機械翻訳》«By default, pgcrypto will now fail to decrypt any messages that were affected in this way. To allow retrieval of such data, a new option ignore-cipher-failure has been added to pgp_pub_decrypt() and pgp_sym_decrypt(). Setting ignore-cipher-failure=1 will restore their previous behavior, allowing the faulty encryption wrapper to be stripped off:» +《機械翻訳》デフォルトにより、pgcryptoはこの方法で影響を受けたメッセージの復号化に失敗するようになりました。 +このようなデータを取得できるようにするために、新しいオプションignore-cipher-failurepgp_pub_decrypt()pgp_sym_decrypt()。 +Setting ignore-cipher-failure=1は以前の動作をリストアし、欠陥のある暗号化ラッパーをオフから取り除くことができますに追加されました。 pgp_sym_decrypt(encrypted_column, any key, 'ignore-cipher-failure=1') @@ -231,7 +238,10 @@ pgp_sym_decrypt(encrypted_column, any key, 'ignore-ci algorithms is not the same, this approach will not work. See the documentation for ignore-cipher-failure. --> -《機械翻訳》«Once the affected messages are identified and stripped of their wrappers, they can then be re-encrypted with a modern algorithm. It is important however that the behavior of OpenSSL be the same as it was when the faulty messages were created: if the set of unsupported algorithms is not the same, this approach will not work. See the documentation for ignore-cipher-failure.» +《機械翻訳》影響を受けたメッセージが特定され、ラッパーが取り除かれると、最新のアルゴリズムで再暗号化できます。 +ただし、OpenSSLの動作が、欠陥のあるメッセージが作成されたときと同じであることが重要です。 +サポートされていないアルゴリズムのセットが同じでない場合、このアプローチは機能しません。 +については、文書を参照してくださいignore-cipher-failure @@ -241,7 +251,7 @@ pgp_sym_decrypt(encrypted_column, any key, 'ignore-ci for reporting this problem. (CVE-2026-14663) --> -《機械翻訳》«The PostgreSQL Project thanks Shishir Sharma for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたShishir Sharmaに感謝します。 (CVE-2026-14663) @@ -271,7 +281,8 @@ Branch: REL_14_STABLE [7215c7e96] 2026-08-10 06:38:36 -0700 the COPY fails before sending PGRES_COPY_IN (Tom Lane) --> -《機械翻訳》«Fix psql to skip in-line data following a scripted COPY ... FROM STDIN command, even if the COPY fails before sending PGRES_COPY_IN » +《機械翻訳》COPYpsqlの送信に失敗した場合でも、スクリプト化されたCOPY ... FROM STDINデータに従って、直線内のコピーコマンドに前を修正する。 +PGRES_COPY_IN (Tom Lane) § § @@ -289,7 +300,9 @@ Branch: REL_14_STABLE [7215c7e96] 2026-08-10 06:38:36 -0700 and to skip data on its own authority if the server doesn't respond with PGRES_COPY_IN. --> -《機械翻訳》«Previously, if a COPY command failed at startup (for instance, because the target table doesn't exist) psql would not realize that and would proceed to read the following in-line data as SQL commands. In the best case that's wrong and in the worst case it's a SQL-injection hazard. Teach psql to recognize syntactically-valid COPY ... FROM STDIN commands and to skip data on its own authority if the server doesn't respond with PGRES_COPY_IN.» +《機械翻訳》これまでは、COPYコマンドがスタートアップで失敗した場合インスタンスにはターゲットテーブルが存在しないためpsqlはそれを認識せず、以下の直線データ内をSQLコマンドとして読み進めていました。 +ベストケースでは間違いであり、最悪のケースではSQLインジェクションハザードです。 +psqlに構文的に有効なコピー.FROM STDINコマンドを認識させ、サーバがPGRES_COPY_INで応答しない場合はスキップデータを権限上でにするように教えました。 @@ -300,7 +313,8 @@ Branch: REL_14_STABLE [7215c7e96] 2026-08-10 06:38:36 -0700 a \. data terminator line after each such command. --> -《機械翻訳》«While this fix is unlikely to affect any production SQL scripts, test scripts might intentionally exercise failing COPY ... FROM STDIN commands. Those will need to gain a \. data terminator line after each such command.» +《機械翻訳》この修正が稼働のSQLスクリプトに影響を与える可能性は低いですが、テストスクリプトが意図的に練習で失敗するコピー.FROM STDINコマンドを実行する可能性があります。 +これらのスクリプトは、このようなコマンドの後に\.データターミネータ直線を取得する必要があります。 @@ -310,7 +324,7 @@ Branch: REL_14_STABLE [7215c7e96] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-6464) --> -《機械翻訳》«The PostgreSQL Project thanks Alexander Lakhin for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたAlexander Lakhinに感謝します。 (CVE-2026-6464) @@ -332,7 +346,7 @@ Branch: REL_14_STABLE [1f49beef2] 2026-08-10 06:38:36 -0700 running EXECUTE or FETCH (Robert Haas) --> -《機械翻訳》«Cross-check the output row type of a portal running EXECUTE or FETCH » +《機械翻訳》クロス-チェックEXECUTEまたはFETCH.を実行する行の出力ポータルタイプ。 (Robert Haas) § @@ -345,7 +359,9 @@ Branch: REL_14_STABLE [1f49beef2] 2026-08-10 06:38:36 -0700 possible to make the declared row types of the two portals diverge, leading to server memory disclosure and arbitrary code execution. --> -《機械翻訳》«EXECUTE and FETCH use two portals: an outer one for the statement itself, and an inner one running the query being executed on its behalf. It was previously possible to make the declared row types of the two portals diverge, leading to server memory disclosure and arbitrary code execution.» +《機械翻訳》EXECUTEおよびFETCH 2つのポータルを使用します。 +1つはステートメント自分自身用の外部ポータルで、もう1つは問い合わせを実行する内部ポータルです。 +以前は、2つのポータルの宣言された行タイプをmakeすることが可能であったため、サーバメモリの開示と任意のコードの実行につながりました。 @@ -356,7 +372,7 @@ Branch: REL_14_STABLE [1f49beef2] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-16239) --> -《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) and Peter Geoghegan for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたBen Morris(Claude and Anthropic Researchと共同で)とPeter Geogheganに感謝します。 (CVE-2026-16239) @@ -378,7 +394,7 @@ Branch: REL_14_STABLE [5fb3c6389] 2026-07-29 17:15:45 +0200 Fix buffer overrun with long time zone abbreviation in to_char() (Tom Lane) --> -《機械翻訳》«Fix buffer overrun with long time zone abbreviation in to_char() » +《機械翻訳》to_char()では、バッファオーバーランを長いタイムゾーン省略形で固定します。 (Tom Lane) § @@ -388,7 +404,7 @@ Branch: REL_14_STABLE [5fb3c6389] 2026-07-29 17:15:45 +0200 This can easily crash the server, and exploits leading to arbitrary code execution have been reported. --> -《機械翻訳》«This can easily crash the server, and exploits leading to arbitrary code execution have been reported.» +《機械翻訳》これは容易にサーバをクラッシュすることができ、恣意的なコードの実行につながるエクスプロイトが報告されている。 @@ -400,7 +416,8 @@ Branch: REL_14_STABLE [5fb3c6389] 2026-07-29 17:15:45 +0200 for reporting this problem. (CVE-2026-14669) --> -《機械翻訳》«The PostgreSQL Project thanks Hcamael, Amjad Shahzad, Tan Zhen of AntAISecurityLab, Tomer Fichman, Zheng Yu, Amy Burnett (OpenAI Codex Security), Rick de Jager, Heewon Song, Sylvie Mayer, Aleksander Alekseev, and Hillai Ben Sasson for reporting this problem. (CVE-2026-14669)» +《機械翻訳》PostgreSQLプロジェクトは、本問題を報告してくれたHcamael、Amjad Shahzad、AntAISecurityLabのTan Zhen、Tomer Fichman、Zheng Yu、Amy Burnett(OpenAI Codexセキュリティ)、Rick de Jager、Heewon Song、Sylvie Mayer、Aleksander Alekseev、およびHillai Ben Sassonに感謝します。 +CVE-2026-14669。 @@ -419,7 +436,7 @@ Branch: REL_14_STABLE [890327639] 2026-08-10 06:38:35 -0700 -《機械翻訳》«Fix buffer overrun in regexp match/split functions » +《機械翻訳》regexpバッファオーバーラン/split関数のマッチを修正しました。 (Masahiko Sawada) § @@ -429,7 +446,7 @@ Branch: REL_14_STABLE [890327639] 2026-08-10 06:38:35 -0700 If passed invalidly-encoded data, these functions could write past the end of their conversion buffer. --> -《機械翻訳》«If passed invalidly-encoded data, these functions could write past the end of their conversion buffer.» +《機械翻訳》不正にエンコードされたデータが渡された場合、これらの関数は変換バッファの末尾を超えて書き込む可能性があります。 @@ -439,7 +456,7 @@ Branch: REL_14_STABLE [890327639] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14664) --> -《機械翻訳》«The PostgreSQL Project thanks Francesco Verardi for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたFrancesco Verardiに感謝します。 (CVE-2026-14664) @@ -460,7 +477,7 @@ Branch: REL_14_STABLE [42d333a78] 2026-08-10 06:38:36 -0700 Harden the ascii() function against invalid input (Michael Paquier) --> -《機械翻訳》«Harden the ascii() function against invalid input » +《機械翻訳》無効な入力に対してascii()関数を固定します。 (Michael Paquier) § @@ -471,7 +488,8 @@ Branch: REL_14_STABLE [42d333a78] 2026-08-10 06:38:36 -0700 to read and return a few bytes of data that it shouldn't. In assert-enabled builds, its assertions could be triggered too. --> -《機械翻訳》«By supplying invalidly-encoded input, this function could be coaxed to read and return a few bytes of data that it shouldn't. In assert-enabled builds, its assertions could be triggered too.» +《機械翻訳》無効にエンコードされた入力を提供することで、この関数は、読み取るべきではない結果の数バイトを読み取り、データするように誘導することができます。 +assertが有効なビルドでは、そのアサーションもトリガされる可能性があります。 @@ -481,7 +499,7 @@ Branch: REL_14_STABLE [42d333a78] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-18024) --> -《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたHcamaelに感謝します。 (CVE-2026-18024) @@ -500,7 +518,7 @@ Branch: REL_18_STABLE [8d428c6e6] 2026-08-10 06:38:12 -0700 in pg_restore_attribute_stats() (OpenAI Security Research Team) --> -《機械翻訳》«Fix multirange type handling in pg_restore_attribute_stats() » +《機械翻訳》複数範囲のタイプハンドリングをpg_restore_attribute_stats()で修正します。 (OpenAI Security Research Team) § § @@ -513,7 +531,8 @@ Branch: REL_18_STABLE [8d428c6e6] 2026-08-10 06:38:12 -0700 for the bounds histogram, but it was wrong for all the other statistics kinds. --> -《機械翻訳》«pg_restore_attribute_stats() treated multirange types just like their underlying range type. This works correctly for the bounds histogram, but it was wrong for all the other statistics kinds.» +《機械翻訳》pg_restore_attribute_stats()は基底の範囲型と同じように多重範囲型を扱いました。 +これは境界のヒストグラムでは正しく動作しますが、他のすべての種類の間違いでは統計処理でした。 @@ -523,7 +542,7 @@ Branch: REL_18_STABLE [8d428c6e6] 2026-08-10 06:38:12 -0700 for reporting this problem. (CVE-2026-16238) --> -《機械翻訳》«The PostgreSQL Project thanks Amy Burnett (OpenAI Codex Security) for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたAmy Burnett(OpenAI Codex Security)に感謝します。 (CVE-2026-16238) @@ -544,7 +563,7 @@ Branch: REL_14_STABLE [59205c7e9] 2026-08-10 06:38:35 -0700 Make scalarineqsel() check that a constant it expects to be of type tid actually is (Tom Lane) --> -《機械翻訳》«Make scalarineqsel() check that a constant it expects to be of type tid actually is » +《機械翻訳》make scalarineqsel()タイプTIDであると予想されるチェックが実際にある定数。 (Tom Lane) § @@ -555,7 +574,7 @@ Branch: REL_14_STABLE [59205c7e9] 2026-08-10 06:38:35 -0700 this estimator, but a maliciously-constructed operator could violate it, leading to a crash or server memory disclosure. --> -《機械翻訳》«This expectation will hold for all the built-in operators that use this estimator, but a maliciously-constructed operator could violate it, leading to a crash or server memory disclosure.» +《機械翻訳》この期待は、この推定量を使用するすべての組み込み演算子に当てはまるが、悪意を持って構築された演算子はそれに違反し、クラッシュやサーバメモリの開示につながる可能性がある。 @@ -565,7 +584,7 @@ Branch: REL_14_STABLE [59205c7e9] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14668) --> -《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたHcamaelに感謝します。 (CVE-2026-14668) @@ -594,7 +613,7 @@ Branch: REL_14_STABLE [1e3014f37] 2026-08-10 06:38:34 -0700 overly long values (both individual lexemes and total vector/query length) (Tom Lane) --> -《機械翻訳》«Harden tsvector and tsquery code against overly long values (both individual lexemes and total vector/query length) » +《機械翻訳》過度に長い値個々のコードとベクタ/問い合わせ語彙素の合計の両方に対して長さtsvectorとtsqueryを強化します。 (Tom Lane) § § @@ -604,7 +623,7 @@ Branch: REL_14_STABLE [1e3014f37] 2026-08-10 06:38:34 -0700 -《機械翻訳》«The documented limits were not enforced in all code paths.» +《機械翻訳》文書化された制限は、コードのすべての経路で実施されたわけではありません。 @@ -614,7 +633,7 @@ Branch: REL_14_STABLE [1e3014f37] 2026-08-10 06:38:34 -0700 for reporting these problems. (CVE-2026-14662) --> -《機械翻訳》«The PostgreSQL Project thanks Yuhang Wu, Zhenpeng Lin, Zheng Yu, and Hcamael for reporting these problems. » +PostgreSQLプロジェクトは、本問題を報告してくれたYuhang Wu、Zhenpeng Lin、Zheng Yu、Hcamaelに感謝します。 (CVE-2026-14662) @@ -643,7 +662,7 @@ Branch: REL_14_STABLE [c7f462838] 2026-08-10 06:38:35 -0700 deal with more than FUNC_MAX_ARGS function arguments (Tom Lane) --> -《機械翻訳》«Fix various places that mistakenly assumed they would not have to deal with more than FUNC_MAX_ARGS function arguments » +《機械翻訳》FUNC_MAX_ARGS関数の議論以上のものを扱う必要がないと誤って想定していたさまざまな場所を修正します。 (Tom Lane) § § @@ -655,7 +674,7 @@ Branch: REL_14_STABLE [c7f462838] 2026-08-10 06:38:35 -0700 aggregate function is FUNC_MAX_ARGS - 1, but the parser failed to enforce that, creating hazards downstream. --> -《機械翻訳》«Notably, the server's actual limit on the number of arguments to an aggregate function is FUNC_MAX_ARGS - 1, but the parser failed to enforce that, creating hazards downstream.» +《機械翻訳》注目すべきことに、サーバの集約関数への引数の数に対する実際の制限はFUNC_MAX_ARGS - 1であるが、パーサはそれを強制することに失敗し、下流に危険をもたらした。 @@ -665,7 +684,7 @@ Branch: REL_14_STABLE [c7f462838] 2026-08-10 06:38:35 -0700 for reporting these problems. (CVE-2026-14679) --> -《機械翻訳》«The PostgreSQL Project thanks Zheng Yu, ylwangtju, and Masahiko Sawada for reporting these problems. » +PostgreSQLプロジェクトは、本問題を報告してくれたZheng Yu、ylwangtju、Masahiko Sawadaに感謝します。 (CVE-2026-14679) @@ -693,7 +712,7 @@ Branch: REL_14_STABLE [913fe0c31] 2026-08-10 06:38:35 -0700 Reject calls from SQL to functions that take or return type internal (Tom Lane) --> -《機械翻訳》«Reject calls from SQL to functions that take or return type internal » +《機械翻訳》SQLから、または戻り値の型内部を取る機能へのコールを拒否します。 (Tom Lane) § § @@ -704,7 +723,7 @@ Branch: REL_14_STABLE [913fe0c31] 2026-08-10 06:38:35 -0700 The existing defenses against doing this have been shown to be insufficient, so add more explicit checks. --> -《機械翻訳》«The existing defenses against doing this have been shown to be insufficient, so add more explicit checks.» +《機械翻訳》これを行うことに対する既存の防御策は不十分であることが示されているため、より明示的なチェックを追加します。 @@ -714,7 +733,7 @@ Branch: REL_14_STABLE [913fe0c31] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14680) --> -《機械翻訳》«The PostgreSQL Project thanks Amy Burnett (OpenAI Codex Security) for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたAmy Burnett(OpenAI Codex Security)に感謝します。 (CVE-2026-14680) @@ -735,7 +754,7 @@ Branch: REL_14_STABLE [70a3b4e18] 2026-08-10 06:38:35 -0700 Preserve the ownership of extended statistics objects when they are rebuilt by ALTER TABLE (Masahiko Sawada) --> -《機械翻訳》«Preserve the ownership of extended statistics objects when they are rebuilt by ALTER TABLE » +《機械翻訳》拡張統計情報オブジェクトがALTER TABLEによって再建された場合、その所有権を保持します。 (Masahiko Sawada) § @@ -745,7 +764,7 @@ Branch: REL_14_STABLE [70a3b4e18] 2026-08-10 06:38:35 -0700 Previously, the role running ALTER TABLE gained ownership of such objects, but that seems inappropriate. --> -《機械翻訳》«Previously, the role running ALTER TABLE gained ownership of such objects, but that seems inappropriate.» +《機械翻訳》以前は、ロールランニングALTER TABLEがそのようなオブジェクトのオーナーシップを取得しましたが、それは不適切なようです。 @@ -755,7 +774,7 @@ Branch: REL_14_STABLE [70a3b4e18] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-6469) --> -《機械翻訳》«The PostgreSQL Project thanks Noah Misch for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたNoah Mischに感謝します。 (CVE-2026-6469) @@ -776,7 +795,7 @@ Branch: REL_14_STABLE [967acab87] 2026-08-10 06:38:35 -0700 When deparsing an EXTRACT() function call, quote the field name if needed (Nathan Bossart) --> -《機械翻訳》«When deparsing an EXTRACT() function call, quote the field name if needed » +《機械翻訳》EXTRACT()関数呼び出しを構文解析する際には、必要に応じてフィールド名前を引用してください。 (Nathan Bossart) § @@ -789,7 +808,8 @@ Branch: REL_14_STABLE [967acab87] 2026-08-10 06:38:35 -0700 during pg_dump), the string body was regurgitated verbatim, allowing SQL injection. --> -《機械翻訳》«The parser accepts any string literal as a field name in EXTRACT(), deferring validation to execution. If the call is stored and deparsed (for example during pg_dump), the string body was regurgitated verbatim, allowing SQL injection.» +《機械翻訳》パーサは、文字列リテラルをフィールド名前として受け入れますEXTRACT()バリデーションの実行を延期します。 +呼び出しが保存され、pg_dumpの間に例に対して解析解除されると、文字列本体が逐語的に戻され、SQLインジェクションが可能になります。 @@ -799,7 +819,7 @@ Branch: REL_14_STABLE [967acab87] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-15741) --> -《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたBen Morris(Claude and Anthropic Researchと共同で)に感謝します。 (CVE-2026-15741) @@ -834,7 +854,7 @@ Branch: REL_14_STABLE [1a358b8f2] 2026-08-10 06:38:36 -0700 Check for USAGE privilege on data types in places that formerly failed to check that (Nathan Bossart) --> -《機械翻訳》«Check for USAGE privilege on data types in places that formerly failed to check that » +《機械翻訳》チェック、以前はそれをチェックすることができなかった場所のUSAGEデータの権限タイプのために。 (Nathan Bossart) § § @@ -850,7 +870,9 @@ Branch: REL_14_STABLE [1a358b8f2] 2026-08-10 06:38:36 -0700 objects depending on the type, possibly blocking the type's owner from changing the type later. --> -《機械翻訳》«CREATE TYPE AS RANGE did not check, nor did ALTER TABLE OF, nor did commands that create stored expressions. These omissions allowed roles without USAGE privilege to nonetheless create objects depending on the type, possibly blocking the type's owner from changing the type later.» +《機械翻訳》CREATEタイプASレンジはチェックを行わず、ALTERテーブルOFも行わず、格納された式を作成するコマンドも行いませんでした。 +これらの省略により、USAGE権限を持たないロールでもタイプに依存するオブジェクトを作成することができました。 +タイプの所有者であるブロッキングは後でタイプを変更する可能性がありました。 @@ -860,7 +882,7 @@ Branch: REL_14_STABLE [1a358b8f2] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-6470) --> -《機械翻訳》«The PostgreSQL Project thanks Jingzhou Fu for reporting this problem.» +PostgreSQLプロジェクトは、本問題を報告してくれたJingzhou Fuに感謝します。 (CVE-2026-6470) @@ -881,7 +903,7 @@ Branch: REL_14_STABLE [f4174aa84] 2026-08-10 06:38:36 -0700 Invalidate role-dependent cached plans after role changes (Ilya Staroverov, Shinya Kato, Nathan Bossart) --> -《機械翻訳》«Invalidate role-dependent cached plans after role changes » +《機械翻訳》ロールの変更後に、ロールに依存するキャッシュ済計画を無効化します。 (Ilya Staroverov, Shinya Kato, Nathan Bossart) § @@ -893,7 +915,7 @@ Branch: REL_14_STABLE [f4174aa84] 2026-08-10 06:38:36 -0700 previously we'd continue to use cached plans that were made according to the old state of affairs. --> -《機械翻訳》«Role membership, role attribute, and database ownership changes may impact the expected behavior of row-level security policies, but previously we'd continue to use cached plans that were made according to the old state of affairs.» +《機械翻訳》ロールメンバシップ、ロール属性、データベースの所有者の変更は、行-レベルセキュリティポリシーの期待される動作をインパクトする可能性がありますが、以前は、古い状況に従って作成されたキャッシュされたプランを引き続き使用していました。 @@ -903,7 +925,7 @@ Branch: REL_14_STABLE [f4174aa84] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-14666) --> -《機械翻訳》«The PostgreSQL Project thanks Ilya Staroverov and Shinya Kato for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたIlya StaroverovとShinya Katoに感謝します。 (CVE-2026-14666) @@ -920,7 +942,7 @@ Branch: REL_17_STABLE [067a64d40] 2026-08-10 06:38:18 -0700 -《機械翻訳》«Reject GSSEncRequest after direct SSL connection » +《機械翻訳》直接SSLコネクション後にGSSEncRequestを拒否します。 (Michael Paquier) § @@ -934,7 +956,9 @@ Branch: REL_17_STABLE [067a64d40] 2026-08-10 06:38:18 -0700 Thus, a pg_hba policy intending to disallow TLS would not be enforced correctly. --> -《機械翻訳》«After establishing a TLS-encrypted connection, the server would still accept a request for GSSAPI encryption. If that succeeded, the connection would proceed using TLS encryption, but it would look like a GSS connection to the pg_hba rules. Thus, a pg_hba policy intending to disallow TLS would not be enforced correctly.» +《機械翻訳》TLSで暗号化されたコネクションを確立した後も、サーバはGSSAPI暗号化用のリクエストを受け入れます。 +それが成功した場合、コネクションはTLS暗号化を使用して処理を進めますが、pg_hbaルールに対してGSSコネクションのように見えます。 +したがって、TLSを許可しないpg_hbaポリシーは正しく適用されません。 @@ -944,7 +968,7 @@ Branch: REL_17_STABLE [067a64d40] 2026-08-10 06:38:18 -0700 for reporting this problem. (CVE-2026-14681) --> -《機械翻訳》«The PostgreSQL Project thanks p4p3r for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたp4p3rに感謝します。 (CVE-2026-14681) @@ -962,7 +986,7 @@ Branch: REL_16_STABLE [fadbe882d] 2026-08-10 06:38:24 -0700 -《機械翻訳》«Make mock SCRAM authentication secrets more plausible » +《機械翻訳》makeはスクラム認証の秘密をよりもっともらしく模倣している。 (Nathan Bossart) § @@ -978,7 +1002,9 @@ Branch: REL_16_STABLE [fadbe882d] 2026-08-10 06:38:24 -0700 instead, to make the mock secret look more like the installation's real secrets. --> -《機械翻訳》«If a SCRAM login is attempted against a role that doesn't exist or doesn't have a SCRAM secret, we generate a mock secret and carry out the authentication handshake anyway, to avoid revealing these facts to an attacker. But the mock secret was made with a fixed iteration count, which in itself can be an observable response discrepancy. Use the configuration setting scram_iterations instead, to make the mock secret look more like the installation's real secrets.» +《機械翻訳》もしSCRAMログインが存在しないかSCRAM秘密を持っていないロールに対して試みられるなら、攻撃者にこれらの事実を明らかにすることを避けるために、模擬秘密を生成し、とにかく認証ハンドシェイクを実行します。 +しかし、模擬秘密は固定反復カウントで作られました、自分自身では観測可能な回答不一致である可能性があります。 +設定設定を使用してくださいscram_iterations代わりに、makeするために模擬秘密はインストールの本当の秘密のように見えます。 @@ -988,7 +1014,7 @@ Branch: REL_16_STABLE [fadbe882d] 2026-08-10 06:38:24 -0700 for reporting this problem. (CVE-2026-14672) --> -《機械翻訳》«The PostgreSQL Project thanks Radim Marek for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたRadim Marekに感謝します。 (CVE-2026-14672) @@ -1010,7 +1036,7 @@ Branch: REL_14_STABLE [74c59d062] 2026-08-10 06:38:35 -0700 applications caused by invalid bytea data received from the server (Michael Paquier) --> -《機械翻訳》«Fix out-of-bounds writes in ecpg applications caused by invalid bytea data received from the server » +《機械翻訳》サーバから受信した無効なbyteaデータによって引き起こされたecpgアプリケーションの範囲外の書き込みを修正しました。 (Michael Paquier) § @@ -1022,7 +1048,8 @@ Branch: REL_14_STABLE [74c59d062] 2026-08-10 06:38:35 -0700 A broken or malicious server might send a string shorter than 2 bytes, resulting in memory clobber in the application. --> -《機械翻訳》«ecpg assumed without checking that any bytea value must begin with \x. A broken or malicious server might send a string shorter than 2 bytes, resulting in memory clobber in the application.» +《機械翻訳》ecpgは、bytea値が\xで始まらなければならないことをチェックせずに想定されます。 +破損した、または悪意のあるサーバは2バイトより短い文字列を送信する可能性があり、その結果、アプリケーションのメモリが破壊されます。 @@ -1032,7 +1059,7 @@ Branch: REL_14_STABLE [74c59d062] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-16241) --> -《機械翻訳》«The PostgreSQL Project thanks ylwangtju for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたylwangtjuに感謝します。 (CVE-2026-16241) @@ -1054,7 +1081,7 @@ Branch: REL_14_STABLE [2006fca40] 2026-08-10 06:38:37 -0700 of psql's \unrestrict command (Nathan Bossart) --> -《機械翻訳》«Do not do backquote expansion on the argument of psql's \unrestrict command » +《機械翻訳》psql\unrestrictコマンドの引数で逆引用符拡張をしないでください。 (Nathan Bossart) § @@ -1067,7 +1094,8 @@ Branch: REL_14_STABLE [2006fca40] 2026-08-10 06:38:37 -0700 running psql, the exact scenario that CVE-2025-8714 intended to prevent. --> -《機械翻訳》«This oversight in the fix for CVE-2025-8714 allows a malicious server to inject shell commands into plain-text dump output that will be run at restore time on the machine running psql, the exact scenario that CVE-2025-8714 intended to prevent.» +《機械翻訳》CVE-2025-8714の修正におけるこの見落としは、悪意のあるサーバが、psql CVE-2025-8714が防止しようとした正確なシェルであるを実行しているテキストでダンプ時間に実行されるプレーンなリストア出力にマシンコマンドを注入することを可能にする。 +シナリオ @@ -1077,7 +1105,7 @@ Branch: REL_14_STABLE [2006fca40] 2026-08-10 06:38:37 -0700 for reporting this problem. (CVE-2026-18408) --> -《機械翻訳》«The PostgreSQL Project thanks Lucas Velgus, Filip Janus, and Daniel Bakker for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたLucas Velgus、Filip Janus、Daniel Bakkerに感謝します。 (CVE-2026-18408) @@ -1100,7 +1128,7 @@ Branch: REL_14_STABLE [57aa21f69] 2026-08-10 06:38:35 -0700 cannot have more than FUNC_MAX_ARGS entries (Tom Lane) --> -《機械翻訳》«Remove pg_dump's assumption that pg_proc.protrftypes cannot have more than FUNC_MAX_ARGS entries » +《機械翻訳》pg_proc.prontftypesFUNC_MAX_ARGSエントリより多く持つことはできないというpg_dumpの仮定を削除しました。 (Tom Lane) § @@ -1116,7 +1144,9 @@ Branch: REL_14_STABLE [57aa21f69] 2026-08-10 06:38:35 -0700 it has. An overrun would lead to a memory clobber inside pg_dump. --> -《機械翻訳》«Since there could be entries for both input and output arguments, it's feasible for this array's length to exceed FUNC_MAX_ARGS (which constrains only input arguments). Even if that were not so, pg_dump cannot assume that the server was built with the same value of FUNC_MAX_ARGS that it has. An overrun would lead to a memory clobber inside pg_dump.» +《機械翻訳》入出力引数の両方に対してエントリが存在する可能性があるため、この配列の長さがFUNC_MAX_ARGS入力引数のみを制約するを超える可能性があります。 +たとえそうでなかったとしても、pg_dumpはサーバがFUNC_MAX_ARGSそれが持つと同じ値で構築されたと想定することはできません。 +オーバーランは内部でメモリを破壊することになりますpg_dump @@ -1126,7 +1156,7 @@ Branch: REL_14_STABLE [57aa21f69] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-19385) --> -《機械翻訳》«The PostgreSQL Project thanks Masahiko Sawada for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたMasahiko Sawadaに感謝します。 (CVE-2026-19385) @@ -1147,7 +1177,7 @@ Branch: REL_14_STABLE [d7fcfead3] 2026-08-10 06:38:35 -0700 Harden PL/Perl against tied Perl arrays and hashes (Tom Lane) --> -《機械翻訳》«Harden PL/Perl against tied Perl arrays and hashes » +《機械翻訳》Perlの配列とハッシュをPL/Perl結合されたものに対して強化する。 (Tom Lane) § @@ -1158,7 +1188,7 @@ Branch: REL_14_STABLE [d7fcfead3] 2026-08-10 06:38:35 -0700 memory overwrite, or to constructing a corrupt result array (which would likely cause problems later). --> -《機械翻訳》«A tied object that doesn't behave like a regular one could lead to memory overwrite, or to constructing a corrupt result array (which would likely cause problems later).» +《機械翻訳》通常のオブジェクトのように動作しないメモリが関連付けられていると、配列が上書きされたり、不正な結果リザルトが構築されたりする可能性があります(これは後で問題を引き起こす可能性があります)。 @@ -1168,7 +1198,7 @@ Branch: REL_14_STABLE [d7fcfead3] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14670) --> -《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたHcamaelに感謝します。 (CVE-2026-14670) @@ -1190,7 +1220,8 @@ Branch: REL_14_STABLE [aff9dac1c] 2026-08-10 06:38:35 -0700 in PL/Perl and PL/Tcl (Heikki Linnakangas) --> -《機械翻訳》«Fix integer overflows in memory-allocation calculations in PL/Perl and PL/Tcl » +《機械翻訳》メモリ内の整数オーバーフローを修正しました。 +PL/PerlPL/Tcl.アロケーション (Heikki Linnakangas) § @@ -1200,7 +1231,7 @@ Branch: REL_14_STABLE [aff9dac1c] 2026-08-10 06:38:35 -0700 This is the same type of problem as CVE-2026-6473, just in a different part of the code, and is fixed in the same way. --> -《機械翻訳》«This is the same type of problem as CVE-2026-6473, just in a different part of the code, and is fixed in the same way.» +《機械翻訳》これは、コードの別のタイプにあるだけで、CVE-2026-6473と同じ問題のパートであり、同じ方法で修正されます。 @@ -1210,7 +1241,7 @@ Branch: REL_14_STABLE [aff9dac1c] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14677) --> -《機械翻訳》«The PostgreSQL Project thanks the Tulya Project (Team Dhiutsa, Bitecope Technologies Private Ltd) for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたTulyaプロジェクト(チームDhiutsa、Bitecope TechnologiesプライベートLtdに感謝します。 (CVE-2026-14677) @@ -1232,7 +1263,7 @@ Branch: REL_14_STABLE [39d792040] 2026-08-10 06:38:36 -0700 restrict search_path before executing index expressions (Noah Misch) --> -《機械翻訳》«Ensure that contrib/amcheck functions restrict search_path before executing index expressions » +《機械翻訳》保証who contrib/amcheck functions restrict search_path前はインデックス式を実行します。 (Noah Misch) § @@ -1247,7 +1278,9 @@ Branch: REL_14_STABLE [39d792040] 2026-08-10 06:38:36 -0700 functions; but if that privilege was granted out, it created a larger hazard than the documentation suggests. --> -《機械翻訳》«Because amcheck will run such index expressions as the owner of their tables, a caller could potentially hijack search_path-dependent functions to run arbitrary code as the table owner. By default this is not a vulnerability because only superusers are allowed to call amcheck functions; but if that privilege was granted out, it created a larger hazard than the documentation suggests.» +《機械翻訳》amcheckはテーブルのインデックス式のような所有者式を実行しますので、呼び出し元が任意のコードをテーブル所有者として実行するためにsearch_path-dependent関数を乗っ取る可能性があります。 +デフォルトではスーパーユーザのみがamcheck関数を呼び出しで実行することを許可されているため、これは弱点ではありません。 +しかし、権限が許可された場合、文書が示唆するよりも大きな危険をもたらしました。 @@ -1257,7 +1290,7 @@ Branch: REL_14_STABLE [39d792040] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-14673) --> -《機械翻訳》«The PostgreSQL Project thanks Yuelin Wang and Jacob Brazeal for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたYuelin WangとJacob Brazealに感謝します。 (CVE-2026-14673) @@ -1280,7 +1313,7 @@ Branch: REL_14_STABLE [9505175f2] 2026-08-10 06:38:36 -0700 and levenshtein_less_equal() functions (Nathan Bossart) --> -《機械翻訳》«Fix integer overflows in contrib/fuzzystrmatch's levenshtein() and levenshtein_less_equal() functions » +《機械翻訳》contrib/fuzzystrmatchlevenshtein()およびlevenshtein_less_equal()関数での整数オーバーフローを修正しました。 (Nathan Bossart) § @@ -1291,7 +1324,7 @@ Branch: REL_14_STABLE [9505175f2] 2026-08-10 06:38:36 -0700 overflows, thereby producing nonsensical results, and even causing out-of-bounds writes in some cases. --> -《機械翻訳》«Passing large cost values to these functions could cause integer overflows, thereby producing nonsensical results, and even causing out-of-bounds writes in some cases.» +《機械翻訳》これらの関数にラージコストの値を渡すと整数オーバーフローが発生し、意味のない結果が生成されたり、場合によっては範囲外の書き込みが発生したりする可能性がありました。 @@ -1301,7 +1334,7 @@ Branch: REL_14_STABLE [9505175f2] 2026-08-10 06:38:36 -0700 for reporting this problem. (CVE-2026-15742) --> -《機械翻訳》«The PostgreSQL Project thanks Ben Morris (in collaboration with Claude and Anthropic Research) for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたBen Morris(Claude and Anthropic Researchと共同で)に感謝します。 (CVE-2026-15742) @@ -1318,7 +1351,7 @@ Branch: REL_18_STABLE [8a31ffc2d] 2026-08-10 06:38:11 -0700 Fix buffer overrun in contrib/pg_stat_statements (Álvaro Herrera) --> -《機械翻訳》«Fix buffer overrun in contrib/pg_stat_statements » +《機械翻訳》バッファオーバーランをcontrib/pg_stat_statements. (Álvaro Herrera) § @@ -1328,7 +1361,7 @@ Branch: REL_18_STABLE [8a31ffc2d] 2026-08-10 06:38:11 -0700 Query normalization didn't accurately account for the amount of space the normalized string would require. --> -《機械翻訳》«Query normalization didn't accurately account for the amount of space the normalized string would require.» +《機械翻訳》問い合わせ正規化は、正規化されたアカウントが必要とするスペースの量に対して、文字列を正確に計算しなかった。 @@ -1338,7 +1371,7 @@ Branch: REL_18_STABLE [8a31ffc2d] 2026-08-10 06:38:11 -0700 for reporting this problem. (CVE-2026-14676) --> -《機械翻訳》«The PostgreSQL Project thanks Sajeeb Lohani (with TrendAI Zero Day Initiative) and Yuelin Wang for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたSajeeb Lohani(TrendAI Zero Day Initiativeと共に)とYuelin Wangに感謝します。 (CVE-2026-14676) @@ -1359,7 +1392,7 @@ Branch: REL_14_STABLE [a74aa0854] 2026-08-10 06:38:35 -0700 Fix datatype error in contrib/pg_trgm's GiST picksplit function (Heikki Linnakangas) --> -《機械翻訳》«Fix datatype error in contrib/pg_trgm's GiST picksplit function » +《機械翻訳》contrib/pg_trgm's GiST picksplitエラーのデータ型関数を修正しました。 (Heikki Linnakangas) § @@ -1370,7 +1403,8 @@ Branch: REL_14_STABLE [a74aa0854] 2026-08-10 06:38:35 -0700 typically causing bad split decisions; but a crash could ensue if you're very unlucky. --> -《機械翻訳》«This mistake resulted in reading past the end of the buffer, typically causing bad split decisions; but a crash could ensue if you're very unlucky.» +《機械翻訳》このミスの結果、バッファの終わりを超えて読むことになり、通常は誤った分割決定が発生しました。 +しかし、非常に運が悪いとクラッシュが起こる可能性があります。 @@ -1380,7 +1414,7 @@ Branch: REL_14_STABLE [a74aa0854] 2026-08-10 06:38:35 -0700 for reporting this problem. (CVE-2026-14678) --> -《機械翻訳》«The PostgreSQL Project thanks Mehmet D. Ince for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたMehmet D. Inceに感謝します。 (CVE-2026-14678) @@ -1400,7 +1434,7 @@ Branch: REL_14_STABLE [5b72d0279] 2026-06-05 12:08:05 -0500 Remove the plan cache in contrib/refint (Ayush Tiwari) --> -《機械翻訳》«Remove the plan cache in contrib/refint » +《機械翻訳》プランのキャッシュを削除しますcontrib/refint。 (Ayush Tiwari) § @@ -1413,7 +1447,9 @@ Branch: REL_14_STABLE [5b72d0279] 2026-06-05 12:08:05 -0500 originally-needed values rather than the key values that should be used. The simplest solution is to remove it. --> -《機械翻訳》«This caching behavior has several serious bugs, notably that check_foreign_key() embeds the new key values in its cascade-UPDATE queries, so a cached plan reuses the originally-needed values rather than the key values that should be used. The simplest solution is to remove it.» +《機械翻訳》このキャッシュ動作にはいくつかの重大なバグがあります。 +特に、check_foreign_key()は新しいキー値をカスケード-更新問い合わせに埋め込むため、キャッシュされたプランは、使用されるべきキー値ではなく、本来必要な値を再利用する。 +最も簡単な解決策は、それを削除することです。 @@ -1423,7 +1459,7 @@ Branch: REL_14_STABLE [5b72d0279] 2026-06-05 12:08:05 -0500 for reporting this problem. (CVE-2026-14671) --> -《機械翻訳》«The PostgreSQL Project thanks Hcamael for reporting this problem. » +PostgreSQLプロジェクトは、本問題を報告してくれたHcamaelに感謝します。 (CVE-2026-14671) @@ -1441,7 +1477,8 @@ Branch: REL_18_STABLE [d4420a972] 2026-07-30 14:59:39 +0200 pg_class.reltuples value correctly (Jan Nidzwetzki, Tomas Vondra) --> -《機械翻訳》«Ensure that parallel GIN index builds update the table's pg_class.reltuples value correctly » +《機械翻訳》パラレルGINインデックスが更新を建設する保証は、テーブルのpg_クラスです。 +reltuples値は正しく。 (Jan Nidzwetzki, Tomas Vondra) § @@ -1461,7 +1498,13 @@ Branch: REL_18_STABLE [d4420a972] 2026-07-30 14:59:39 +0200 their reltuples entries look sane. A query such as this may be helpful: --> -《機械翻訳》«A parallel worker could report an uninitialized value for the number of rows it processed, leading to a bogus value for reltuples, even Infinity or NaN. Such values could lead to subsequent autovacuum and autoanalyze operations never deciding that the table needs to be processed. If so, the situation will not self-heal. A manual ANALYZE command, or creation of another index, will be needed to reset reltuples to the correct value. If you have any tables with GIN indexes, it's recommended to check to see if their reltuples entries look sane. A query such as this may be helpful:» +《機械翻訳》パラレルワーカーは、処理した行数の初期化されていない値をレポートし、reltuples InfinityやNaNであっても偽の値をもたらす可能性があります。 +このような値は、後続のオートバキュームおよび自動分析操作で、テーブルニーズを処理することを決定しない可能性があります。 +その場合、シチュエーションは自己修復しません。 +マニュアルreltuplesコマンド、または別のインデックスの作成は、正しい値にリセットするために必要になります。 +GINインデックスを持つテーブルがある場合は、チェックにreltuplesエントリが正常に見えるかどうかを確認することをお勧めします。 +次のような問い合わせが役立つ場合があります。 +ANALYZE SELECT DISTINCT t.oid::regclass, t.reltuples FROM pg_class t @@ -1489,7 +1532,8 @@ Branch: REL_14_STABLE [cec48686a] 2026-08-07 17:30:06 +0900 asynchronous Append plan node (Alexander Korotkov, Gleb Kashkin, Etsuro Fujita) --> -《機械翻訳》«Fix mis-handling of asynchronous reads when rescanning an asynchronous Append plan node » +《機械翻訳》非同期Appendプランのハンドリングを再スキャンする際に、ノードの読み取りが正しく行われない問題を修正しました。 +非同期 (Alexander Korotkov, Gleb Kashkin, Etsuro Fujita) § @@ -1504,7 +1548,9 @@ Branch: REL_14_STABLE [cec48686a] 2026-08-07 17:30:06 +0900 scan. The outcome could be incorrect query results, an infinite loop, or an assertion failure. --> -《機械翻訳》«When an upper plan node rescans an Append before having read the entire Append output, we need to discard any in-flight requests sent to external servers (by postgres_fdw for example). This was not done correctly in cases where a subplan has parameter changes or is discarded by partition pruning in the next scan. The outcome could be incorrect query results, an infinite loop, or an assertion failure.» +《機械翻訳》上位プランノードがAppend前を再スキャンしてAppendの出力全体を読み込んだら、外部サーバに送信されたin-フライト要求を破棄する必要がありますpostgres_fdw by for例)。 +これは、サブプランにパラメータの変更がある場合や、次のスキャンのパーティションプルーニングによって破棄される場合には正しく行われませんでした。 +結果は、問い合わせの結果が正しくない、無限ループ、またはアサーションの失敗となる可能性があります。 @@ -1524,7 +1570,7 @@ Branch: REL_14_STABLE [6098f35f4] 2026-07-31 15:39:21 +1200 Fix error in partition pruning for RANGE-partitioned tables (David Rowley) --> -《機械翻訳》«Fix error in partition pruning for RANGE-partitioned tables » +《機械翻訳》レンジ~テーブルパーティションのパーティション剪定でエラーを固定。 (David Rowley) § @@ -1534,7 +1580,7 @@ Branch: REL_14_STABLE [6098f35f4] 2026-07-31 15:39:21 +1200 In some cases the DEFAULT partition would be skipped when it should not be, which could lead to rows missing from query results. --> -《機械翻訳》«In some cases the DEFAULT partition would be skipped when it should not be, which could lead to rows missing from query results.» +《機械翻訳》場合によっては、デフォルトパーティションが本来でないときにスキップされたになり、問い合わせの結果から行が欠落することがあります。 @@ -1550,7 +1596,7 @@ Branch: REL_18_STABLE [bba4e095d] 2026-06-25 12:15:02 +0900 Correctly update foreign-data-wrapper state in a ModifyTable plan node after pruning result relations (Ayush Tiwari, Rafia Sabih) --> -《機械翻訳》«Correctly update foreign-data-wrapper state in a ModifyTable plan node after pruning result relations » +《機械翻訳》結果のラッパーをプルーニングした後、ModifyTableプランノードで外国-データ-リレーションの状態が正しく更新されます。 (Ayush Tiwari, Rafia Sabih) § § @@ -1563,7 +1609,7 @@ Branch: REL_18_STABLE [bba4e095d] 2026-06-25 12:15:02 +0900 and the table had any foreign-table partitions, a crash or erroneous behavior was likely. --> -《機械翻訳》«Previously, if run-time partition pruning determined that some partitions of a partitioned target table need not be scanned and the table had any foreign-table partitions, a crash or erroneous behavior was likely.» +《機械翻訳》以前は、実行時のパーティションプルーニングによって、分割されたターゲットテーブルの一部のパーティションをスキャンする必要がないと判断され、そのテーブルに外部テーブルパーティションがある場合、クラッシュまたは誤った動作が発生する可能性があった。 @@ -1580,7 +1626,8 @@ Branch: REL_18_STABLE [4908225be] 2026-07-08 20:46:26 +0100 with RETURNING OLD on a table that has a BEFORE UPDATE trigger (Dean Rasheed) --> -《機械翻訳》«Fix missed concurrent update in UPDATE with RETURNING OLD on a table that has a BEFORE UPDATE trigger » +《機械翻訳》BEFORE UPDATEトリガを持つテーブル上のwith RETURNING OLDで同時更新が失敗する問題を修正しました。 +UPDATE (Dean Rasheed) § @@ -1594,7 +1641,8 @@ Branch: REL_18_STABLE [4908225be] 2026-07-08 20:46:26 +0100 (although the trigger itself, and the final output row, saw the correct values). --> -《機械翻訳》«If the target row was concurrently updated, then at isolation level READ COMMITTED any OLD values in RETURNING should reflect the updated row. But stale values were returned if there was a trigger (although the trigger itself, and the final output row, saw the correct values).» +《機械翻訳》ターゲット行が同時に更新された場合、隔離レベルでREAD COMMITTED RETURNING内の任意のOLD値は更新された行を反映する必要があります。 +しかし、トリガがある場合は古い値が返されました(ただし、トリガ自分自身と最終的な出力行には正しい値が表示されました)。 @@ -1610,7 +1658,7 @@ Branch: REL_18_STABLE [f70acc8a2] 2026-07-31 23:24:46 +1200 Fix hash join performance issue when there are multiple join keys and many NULL values (David Rowley) --> -《機械翻訳》«Fix hash join performance issue when there are multiple join keys and many NULL values » +《機械翻訳》ハッシュ結合パフォーマンスキーと多くのNULL値がある場合のマルチプル結合問題を修正しました。 (David Rowley) § @@ -1622,7 +1670,9 @@ Branch: REL_18_STABLE [f70acc8a2] 2026-07-31 23:24:46 +1200 the null was in a non-last join column, bloating the hash table quite a lot if many inputs contain nulls. --> -《機械翻訳》«Null-keyed tuples should not get inserted into the hash table, since they will never match any other tuples. The code got this wrong if the null was in a non-last join column, bloating the hash table quite a lot if many inputs contain nulls.» +《機械翻訳》NULLキー付きタプルはハッシュテーブルに挿入されるべきではありません。 +なぜなら、それらは他のタプルをマッチにすることは決してないからです。 +コードは、NULLが最後以外の結合カラムにあった場合にこの間違いを取得し、多くの入力にNULLが含まれている場合にハッシュテーブルをかなり膨張させます。 @@ -1638,7 +1688,7 @@ Branch: REL_18_STABLE [9108fed3e] 2026-06-11 12:08:48 +0100 parenthesized OLD/NEW in RETURNING expressions (Marko Grujic) --> -《機械翻訳》«Fix parsing of parenthesized OLD/NEW in RETURNING expressions » +《機械翻訳》RETURNINGエクスプレッション内でカッコで囲まれたOLD/NEWのパースを修正しました。 (Marko Grujic) § @@ -1649,7 +1699,7 @@ Branch: REL_18_STABLE [9108fed3e] 2026-06-11 12:08:48 +0100 and (old).* were mis-handled, effectively converting them to NEW references. --> -《機械翻訳》«Expressions such as (old).colname and (old).* were mis-handled, effectively converting them to NEW references.» +《機械翻訳》(old).colname(old).*などの式は誤って処理され、事実上NEW参照に変換されました。 @@ -1671,7 +1721,7 @@ Branch: REL_14_STABLE [13b627a3e] 2026-07-28 16:09:04 -0400 (array) expressions (Ayush Tiwari) --> -《機械翻訳》«Fix planner's nullability and strictness checks for value IN (array) expressions » +《機械翻訳》プランナのvalue IN (array)式に対するNULL許容性と厳密性のチェックを修正した。 (Ayush Tiwari) § @@ -1683,7 +1733,8 @@ Branch: REL_14_STABLE [13b627a3e] 2026-07-28 16:09:04 -0400 to be applied that should not be. This could result in wrong query answers if the array actually was empty. --> -《機械翻訳》«These checks should only succeed if the array operand is known to be non-empty, but that consideration was missed, allowing optimizations to be applied that should not be. This could result in wrong query answers if the array actually was empty.» +《機械翻訳》これらのチェックは、配列オペランドが空でないことがわかっているが、考慮されていないため、本来適用されるべきではない最適化が適用された場合にのみ成功します。 +これにより、間違い問い合わせが実際に空の場合に、配列地域の回答が得られる可能性があります。 @@ -1706,7 +1757,7 @@ Branch: REL_16_STABLE [d610d8e8b] 2026-07-20 12:17:40 +0900 -《機械翻訳》«Fix incorrect join removal logic » +《機械翻訳》誤った結合取外しロジックを修正する。 (Matheus Alcantara, Richard Guo) § § @@ -1718,7 +1769,7 @@ Branch: REL_16_STABLE [d610d8e8b] 2026-07-20 12:17:40 +0900 from within the nullable side of an outer join to not be replaced by NULL when it should be. --> -《機械翻訳》«In edge cases, it was possible for a constant output value coming from within the nullable side of an outer join to not be replaced by NULL when it should be.» +《機械翻訳》エッジケースでは、定数のNull許容側の内部から取得した外部結合の出力の値が、NULLによって置換されるべきときに置換されない可能性がありました。 @@ -1736,7 +1787,7 @@ Branch: REL_18_STABLE [18105e6db] 2026-07-15 09:22:58 +0900 Clean up PlaceHolderVars more thoroughly during join removal (Richard Guo, Arne Roland) --> -《機械翻訳》«Clean up PlaceHolderVars more thoroughly during join removal » +《機械翻訳》結合の撤去作業中は、PlaceHolderVarsをより徹底的に清掃してください。 (Richard Guo, Arne Roland) § § @@ -1747,7 +1798,7 @@ Branch: REL_18_STABLE [18105e6db] 2026-07-15 09:22:58 +0900 This fix corrects various edge cases that could trip assertions or result in incorrect plans. --> -《機械翻訳》«This fix corrects various edge cases that could trip assertions or result in incorrect plans.» +《機械翻訳》この修正では、アサーションをトリップしたり、誤った計画を生成する可能性のあるさまざまなエッジケースが修正されている。 @@ -1767,7 +1818,7 @@ Branch: REL_14_STABLE [64778fac7] 2026-06-08 11:48:18 -0400 container datatypes (arrays, composites, ranges) (Andrei Lepikhov, Tom Lane) --> -《機械翻訳》«Add missed checks for hashability of equality comparisons on container datatypes (arrays, composites, ranges) » +《機械翻訳》コンテナデータ型(配列、コンポジット、範囲)での等価性比較のハッシュ可能性のチェック漏れが追加された。 (Andrei Lepikhov, Tom Lane) § @@ -1779,7 +1830,8 @@ Branch: REL_14_STABLE [64778fac7] 2026-06-08 11:48:18 -0400 step was missed in some places, leading to could not identify a hash function failures at execution. --> -《機械翻訳》«The planner must verify hashability of the container's component type(s) before deciding it can use a hash-based plan type. This step was missed in some places, leading to could not identify a hash function failures at execution.» +《機械翻訳》プランナは、コンテナのコンポーネントタイプ前のハッシュ可能性を検証し、ハッシュを拠点とするプランタイプを使用できることを決定する必要があります。 +このステップがいくつかの場所で欠落していたため、実行時に失敗しましたハッシュ機能を識別できませんでした。 @@ -1795,7 +1847,7 @@ Branch: REL_18_STABLE [fe5d62951] 2026-07-06 16:15:45 +0900 Avoid pushing WHERE clauses down past a grouping step that has a different equivalence rule (Richard Guo) --> -《機械翻訳》«Avoid pushing WHERE clauses down past a grouping step that has a different equivalence rule » +《機械翻訳》同等ダウンが異なるグループ化ステップを通過してWHERE条項ルールをプッシュすることは避けてください。 (Richard Guo) § @@ -1807,7 +1859,8 @@ Branch: REL_18_STABLE [fe5d62951] 2026-07-06 16:15:45 +0900 same collation. Otherwise it might filter some rows the grouping would have merged. --> -《機械翻訳》«A test on a grouping column that is grouped by a nondeterministic collation is safe to push down only if it is a comparison using that same collation. Otherwise it might filter some rows the grouping would have merged.» +《機械翻訳》非決定性テストによってグループ化されたグループ化カラムの照合順序は、同じ照合順序を使用する比較である場合にのみ、セーフからプッシュダウンになります。 +そうでない場合は、グループ化がマージしたはずの行をフィルタする可能性があります。 @@ -1827,7 +1880,7 @@ Branch: REL_15_STABLE [842e34efa] 2026-07-08 00:00:34 +1200 that have an EXCLUDE clause or lack ORDER BY (Chengpeng Yan, David Rowley) --> -《機械翻訳》«Fix mis-optimization of COUNT window functions that have an EXCLUDE clause or lack ORDER BY » +《機械翻訳》EXCLUDE最適化または欠損ORDER BYを持つCOUNTウィンドウ関数の句ミスを修正した。 (Chengpeng Yan, David Rowley) § @@ -1837,7 +1890,7 @@ Branch: REL_15_STABLE [842e34efa] 2026-07-08 00:00:34 +1200 These window functions were treated as monotonic when they should not be, allowing wrong answers to be computed. --> -《機械翻訳》«These window functions were treated as monotonic when they should not be, allowing wrong answers to be computed.» +《機械翻訳》これらの窓関数は、単調であるべきでないときに単調として扱われ、間違いの回答を計算することを可能にした。 @@ -1853,7 +1906,7 @@ Branch: REL_18_STABLE [5fd1c3f28] 2026-06-28 12:31:29 -0400 planner looks up statistics for a column of type "char" (Feng Wu) --> -《機械翻訳》«Avoid cache lookup failed for collation 0 error when planner looks up statistics for a column of type "char" » +《機械翻訳》プランナが統計処理でタイプ"char"のキャッシュを検索する場合は、照合順序0のカラム検索に失敗エラーを回避します。 (Feng Wu) § @@ -1875,7 +1928,7 @@ Branch: REL_14_STABLE [cad17745e] 2026-08-04 09:06:46 +0200 Fix ALTER COLUMN ... DROP EXPRESSION to work when there are multiple levels of partitions (Alberto Piai) --> -《機械翻訳》«Fix ALTER COLUMN ... DROP EXPRESSION to work when there are multiple levels of partitions » +《機械翻訳》ALTER COLUMN ... DROP EXPRESSIONマルチプルレベルの間仕切りがある場合に機能するように修正します。 (Alberto Piai) § @@ -1894,7 +1947,7 @@ Branch: REL_17_STABLE [1d6c654c8] 2026-07-20 17:21:20 +0200 Fix attaching partitions of indexes that are exclusion constraints (Japin Li) --> -《機械翻訳》«Fix attaching partitions of indexes that are exclusion constraints » +《機械翻訳》排他制約であるインデックスのパーティションの付加を修正しました。 (Japin Li) § @@ -1904,7 +1957,7 @@ Branch: REL_17_STABLE [1d6c654c8] 2026-07-20 17:21:20 +0200 Notably, this oversight broke dump/restore of partitioned exclusion constraints. --> -《機械翻訳》«Notably, this oversight broke dump/restore of partitioned exclusion constraints.» +《機械翻訳》特に、この見落としは、分割された排他制約のダンプ/リストアを破壊した。 @@ -1920,7 +1973,7 @@ Branch: REL_18_STABLE [41247cdf6] 2026-05-23 00:01:24 +0900 partitioned NOT NULL constraints via ALTER CONSTRAINT (Andreas Karlsson) --> -《機械翻訳》«Prevent setting NO INHERIT on partitioned NOT NULL constraints via ALTER CONSTRAINT » +《機械翻訳》設定を禁止するNO INHERITパーティション化されたNOT NULL ALTER CONSTRAINTによる制約。 (Andreas Karlsson) § @@ -1933,7 +1986,8 @@ Branch: REL_18_STABLE [41247cdf6] 2026-05-23 00:01:24 +0900 enforced by constraint creation, but not by ALTER TABLE ... ALTER CONSTRAINT. --> -《機械翻訳》«NOT NULL constraints on partitioned tables are supposed to be inherited by all partitions, and therefore must not be marked NO INHERIT. This rule was correctly enforced by constraint creation, but not by ALTER TABLE ... ALTER CONSTRAINT.» +《機械翻訳》NOT NULLテーブルパーティションの制約はすべてのパーティションに継承されることになっているため、NO INHERITとマークされてはなりません。 +このルールは制約の作成時に正しく適用されましたが、ALTER TABLE ... ALTER CONSTRAINTでは適用されませんでした。 @@ -1952,7 +2006,7 @@ Branch: REL_14_STABLE [1b17a6e3c] 2026-07-04 11:34:26 -0400 -《機械翻訳》«Disallow renaming a rule to _RETURN » +《機械翻訳》ルール名を_RETURN. (Tom Lane) § @@ -1963,7 +2017,7 @@ Branch: REL_14_STABLE [1b17a6e3c] 2026-07-04 11:34:26 -0400 rule, but ALTER RULE allowed renaming other rules to _RETURN, causing trouble later. --> -《機械翻訳》«That name is reserved for a view's ON SELECT rule, but ALTER RULE allowed renaming other rules to _RETURN, causing trouble later.» +《機械翻訳》その名前はビューの予約であるON SELECTルールだが、ALTERルールは他のルールを_RETURNに改名することを許可し、後にトラブルを引き起こした。 @@ -1981,7 +2035,7 @@ Branch: REL_16_STABLE [e61d44fde] 2026-08-03 12:25:49 -0700 Fix missing lock release for role membership grants in DROP OWNED BY (Jeff Davis) --> -《機械翻訳》«Fix missing lock release for role membership grants in DROP OWNED BY » +《機械翻訳》DROP OWNED BYで欠落しているロックリリースのロールメンバシップ認可を修正します。 (Jeff Davis) § @@ -1991,7 +2045,7 @@ Branch: REL_16_STABLE [e61d44fde] 2026-08-03 12:25:49 -0700 This oversight resulted in a warning message, followed by retaining a lock on the membership grant until the end of the transaction. --> -《機械翻訳》«This oversight resulted in a warning message, followed by retaining a lock on the membership grant until the end of the transaction.» +《機械翻訳》この見落としの結果、ワーニングメッセージが誕生し、その後、トランザクションの終わりまでメンバシップ助成金のロックが維持されました。 @@ -2009,7 +2063,7 @@ Branch: REL_16_STABLE [485527190] 2026-07-08 08:51:09 +0900 Fix failure of EXPLAIN when deparsing SQL/JSON aggregates (Richard Guo) --> -《機械翻訳》«Fix failure of EXPLAIN when deparsing SQL/JSON aggregates » +《機械翻訳》EXPLAINdeparse SQL/JSON aggregates.*の失敗を修正しました。 (Richard Guo) § @@ -2019,7 +2073,7 @@ Branch: REL_16_STABLE [485527190] 2026-07-08 08:51:09 +0900 Some plan structures resulted in invalid JsonConstructorExpr underlying node type errors. --> -《機械翻訳》«Some plan structures resulted in invalid JsonConstructorExpr underlying node type errors.» +《機械翻訳》一部のプラン構造でノードタイプの基礎となる無効なJsonConstructorExprエラーが発生しました。 @@ -2039,7 +2093,7 @@ Branch: REL_14_STABLE [55adef7ab] 2026-07-28 08:35:21 +0900 Fix use of REINDEX CONCURRENTLY with a deferred uniqueness constraint (Nitin Motiani) --> -《機械翻訳》«Fix use of REINDEX CONCURRENTLY with a deferred uniqueness constraint » +《機械翻訳》REINDEX CONCURRENTLY with a遅延一意性制約の使用を修正しました。 (Nitin Motiani) § @@ -2050,7 +2104,8 @@ Branch: REL_14_STABLE [55adef7ab] 2026-07-28 08:35:21 +0900 CONCURRENTLY was incorrectly marked as enforcing immediate uniqueness, causing spurious reports of constraint violation. --> -《機械翻訳》«The transient index copy created during REINDEX CONCURRENTLY was incorrectly marked as enforcing immediate uniqueness, causing spurious reports of constraint violation.» +《機械翻訳》の間に作成された一時的なインデックスコピーは、即時の一意性を強制するものとして誤ってマークされ、制約違反の誤った報告を引き起こした。 +REINDEX CONCURRENTLY @@ -2069,7 +2124,7 @@ Branch: REL_18_STABLE [51652c42d] 2026-07-06 14:47:58 -0400 Fix LIKE matching with nondeterministic collations and backslashes (Nitin Motiani, Tom Lane) --> -《機械翻訳》«Fix LIKE matching with nondeterministic collations and backslashes » +《機械翻訳》照合とバックスラッシュが明確でないLIKEマッチングを修正しました。 (Nitin Motiani, Tom Lane) § § @@ -2085,7 +2140,9 @@ Branch: REL_18_STABLE [51652c42d] 2026-07-06 14:47:58 -0400 character to be matched exactly rather than allowing the nondeterministic collation to decide if there's a match. --> -《機械翻訳》«When using a nondeterministic collation, LIKE mishandled an escaped backslash (\\), treating it as effectively not there. It also did the wrong thing with a leading backslash preceding an ordinary character; in that case the backslash should be effectively ignored, but it caused the ordinary character to be matched exactly rather than allowing the nondeterministic collation to decide if there's a match.» +《機械翻訳》非決定性照合順序を使用する場合、LIKEエスケープされたバックスラッシュ\\を誤って処理し、実質的に存在しないものとして処理しました。 +また、通常の文字の前に間違いが先行するバックスラッシュの処理も行いました。 +このケースではバックスラッシュは実質的に無視されるはずですが、非決定性照合順序がマッチがあるかどうかを決定できるようにするのではなく、通常の文字が正確に一致するようにしました。 @@ -2101,7 +2158,7 @@ Branch: REL_18_STABLE [d0bb49e61] 2026-07-06 13:06:25 -0400 Fix LIKE/regex optimization for indexscan with exact-match pattern (Jelte Fennema-Nio) --> -《機械翻訳》«Fix LIKE/regex optimization for indexscan with exact-match pattern » +《機械翻訳》正確な最適化マッチを使用するインデックススキャン用のLIKE/regexパターンを修正しました。 (Jelte Fennema-Nio) § @@ -2117,7 +2174,9 @@ Branch: REL_18_STABLE [d0bb49e61] 2026-07-06 13:06:25 -0400 \d tablename command much slower. --> -《機械翻訳》«Refactoring for LIKE with non-deterministic collations accidentally broke the optimization for converting a LIKE or regex exact-match pattern to an equality index condition when the index collation doesn't match the expression collation. Among other things, that made psql's \d tablename command much slower.» +《機械翻訳》LIKE非決定性照合を使用したのリファクタリングでは、最適化パターンがマッチインデックスをマッチしていない場合に、LIKEまたは正規表現の正確な式照合順序を等式条件に変換するためのが誤って壊れた。 +とりわけ、psql\d tablenameコマンドははるかに遅くなった。 +インデックス照合順序 @@ -2139,7 +2198,7 @@ Branch: REL_18_STABLE [5f003855e] 2026-08-11 21:24:28 +0300 Fix matching of localized month/day names in to_date() (Heikki Linnakangas) --> -《機械翻訳》«Fix matching of localized month/day names in to_date() » +《機械翻訳》ローカライズされた月/日の名前のマッチングを修正しましたto_date(). (Heikki Linnakangas) § § @@ -2150,7 +2209,7 @@ Branch: REL_18_STABLE [5f003855e] 2026-08-11 21:24:28 +0300 The matching logic misbehaved in cases where case-folding changes the byte length of the string. --> -《機械翻訳》«The matching logic misbehaved in cases where case-folding changes the byte length of the string.» +《機械翻訳》マッチングロジックでは、ケース折りによって文字列のバイト長さが変更された場合に問題が発生した。 @@ -2165,7 +2224,7 @@ Branch: REL_18_STABLE [66ec24276] 2026-07-07 15:04:31 -0700 -《機械翻訳》«Correct case-folding rules for Greek final sigma » +《機械翻訳》ケース最終シグマのギリシャフォールディング規則を修正する。 (Jeff Davis) § @@ -2176,7 +2235,8 @@ Branch: REL_18_STABLE [66ec24276] 2026-07-07 15:04:31 -0700 consider it to be a final sigma. This only affects the built-in pg_unicode_fast locale. --> -《機械翻訳》«If the string is preceded only by Case Ignorable characters, don't consider it to be a final sigma. This only affects the built-in pg_unicode_fast locale.» +《機械翻訳》文字列の前にケースIgnorable文字しかない場合、それを最終シグマとはみなさない。 +これは組み込みのpg_unicode_fastロケールにのみ影響する。 @@ -2195,7 +2255,7 @@ Branch: REL_14_STABLE [8bb935d61] 2026-06-05 07:50:18 +0900 Fix incorrect NFC recomposition for Hangul U+11A7 (TBASE) (Diego Frias, Michael Paquier) --> -《機械翻訳》«Fix incorrect NFC recomposition for Hangul U+11A7 (TBASE) » +《機械翻訳》ハングルU+11A7(TBASE)の不正なNFC再構成を修正します。 (Diego Frias, Michael Paquier) § @@ -2205,7 +2265,7 @@ Branch: REL_14_STABLE [8bb935d61] 2026-06-05 07:50:18 +0900 This character was treated as a valid T syllable, which it is not, and hence silently swallowed during normalization. --> -《機械翻訳》«This character was treated as a valid T syllable, which it is not, and hence silently swallowed during normalization.» +《機械翻訳》この文字は有効なT音節として扱われましたが、実際はそうではないため、正規化中に静かに飲み込まれました。 @@ -2224,7 +2284,7 @@ Branch: REL_14_STABLE [1e0458172] 2026-06-08 11:49:27 -0700 Avoid possible truncation of output lexemes in case-insensitive synonym dictionaries (Jeff Davis) --> -《機械翻訳》«Avoid possible truncation of output lexemes in case-insensitive synonym dictionaries » +《機械翻訳》ケースの影響を受けないsynonym辞書語彙素では、出力が切り捨てられる可能性があります。 (Jeff Davis) § @@ -2234,7 +2294,7 @@ Branch: REL_14_STABLE [1e0458172] 2026-06-08 11:49:27 -0700 If folding to lower case increased the byte length of a lexeme, it was incorrectly truncated to its original byte length when emitted. --> -《機械翻訳》«If folding to lower case increased the byte length of a lexeme, it was incorrectly truncated to its original byte length when emitted.» +《機械翻訳》下部ケースへの折り畳みが語彙素のバイト長さを増加させた場合、放出時にオリジナルバイト長さに誤って切り捨てられた。 @@ -2251,7 +2311,7 @@ Branch: REL_17_STABLE [5e78ebca5] 2026-07-07 13:35:23 -0700 Defend against truncated UTF-8 characters in case-conversion logic (Jeff Davis) --> -《機械翻訳》«Defend against truncated UTF-8 characters in case-conversion logic » +《機械翻訳》ケース-変換ロジックで切り捨てられたUTF-8文字に対する防御。 (Jeff Davis) § @@ -2271,7 +2331,7 @@ Branch: REL_14_STABLE [74d3482f4] 2026-06-03 12:47:34 +0900 -《機械翻訳》«Fix typo in hash_record_extended() » +《機械翻訳》タイポをhash_record_extended(). (Man Zeng) § @@ -2284,7 +2344,9 @@ Branch: REL_14_STABLE [74d3482f4] 2026-06-03 12:47:34 +0900 However, extension-provided hash functions could be affected if they inspect PG_ARGISNULL(1). --> -《機械翻訳》«The code failed to initialize the second isnull argument passed to FunctionCallInvoke(). This is harmless for existing in-core extended hash support functions, which will not examine that value. However, extension-provided hash functions could be affected if they inspect PG_ARGISNULL(1).» +《機械翻訳》コードは、FunctionCallInvoke()に渡された2番目のisnull引数の初期化に失敗しました。 +これは、その値を検査しない既存の-コア拡張ハッシュサポート関数には問題ありません。 +ただし、extension提供のハッシュ関数は、検査PG_ARGISNULL(1)。 @@ -2302,7 +2364,7 @@ Branch: REL_16_STABLE [6c760f6b6] 2026-07-28 10:39:50 -0700 Fix pg_get_publication_tables() to not fail if a publishable table is dropped concurrently (Bharath Rupireddy) --> -《機械翻訳》«Fix pg_get_publication_tables() to not fail if a publishable table is dropped concurrently » +《機械翻訳》pg_get_publication_tables()発行可能なテーブルが同時に削除されても失敗しないように修正しました。 (Bharath Rupireddy) § @@ -2324,7 +2386,7 @@ Branch: REL_14_STABLE [0115650de] 2026-07-06 12:24:28 -0400 Prevent satisfies_hash_partition() from crashing with VARIADIC NULL (Robert Haas) --> -《機械翻訳》«Prevent satisfies_hash_partition() from crashing with VARIADIC NULL » +《機械翻訳》satisfies_hash_partition()VARIADIC NULL. (Robert Haas) § @@ -2346,7 +2408,7 @@ Branch: REL_14_STABLE [262cc4df2] 2026-06-04 12:24:51 -0400 in tsvector_filter() and allied functions (Ewan Young) --> -《機械翻訳》«Report invalid-weight errors more cleanly and consistently in tsvector_filter() and allied functions » +《機械翻訳》レポートが無効-ウェイトは、tsvector_filter()および関連する関数において、より明確かつ一貫してエラーを発生させる。 (Ewan Young) § @@ -2358,7 +2420,8 @@ Branch: REL_14_STABLE [262cc4df2] 2026-06-04 12:24:51 -0400 as charout() would render them. This avoids possibly producing an invalidly-encoded error message. --> -《機械翻訳》«In particular, report weight characters that are not printable ASCII in octal form (\nnn), as charout() would render them. This avoids possibly producing an invalidly-encoded error message.» +《機械翻訳》特に、\nnnのように、8進数のウェイトで表示可能なASCII文字ではないレポートフォーム文字は、charout()表示されます。 +これにより、無効にエンコードされたエラーメッセージが生成される可能性がなくなります。 @@ -2374,7 +2437,7 @@ Branch: REL_18_STABLE [c31b0fca0] 2026-07-16 11:50:13 -0700 Reject out-of-range timestamp shift values in uuidv7() (Baji Shaik) --> -《機械翻訳》«Reject out-of-range timestamp shift values in uuidv7() » +《機械翻訳》uuidv7().内のレンジ外タイムスタンプシフト値を拒否します。 (Baji Shaik) § @@ -2385,7 +2448,8 @@ Branch: REL_18_STABLE [c31b0fca0] 2026-07-16 11:50:13 -0700 of the range that a v7 UUID can represent. Previously, a garbage UUID value was produced. --> -《機械翻訳》«The shift value must not be so large as to produce a timestamp out of the range that a v7 UUID can represent. Previously, a garbage UUID value was produced.» +《機械翻訳》シフト値は、v7ラージが表現できるタイムスタンプからレンジを生成するようなUUIDであってはなりません。 +以前は、ガーベッジUUID値が生成されていました。 @@ -2410,7 +2474,7 @@ Branch: REL_14_STABLE [a17f39aa2] 2026-06-12 12:39:40 +0900 Fix mishandling of namespace nodes in xpath() (Michael Paquier) --> -《機械翻訳》«Fix mishandling of namespace nodes in xpath() » +《機械翻訳》xpath()でのネームスペースノードの誤った処理を修正しました。 (Michael Paquier) § § @@ -2421,7 +2485,7 @@ Branch: REL_14_STABLE [a17f39aa2] 2026-06-12 12:39:40 +0900 This fix avoids an unexpected could not copy node error. --> -《機械翻訳》«This fix avoids an unexpected could not copy node error.» +《機械翻訳》この修正により、予期しないノードをコピーできないエラーが回避されます。 @@ -2440,7 +2504,7 @@ Branch: REL_17_STABLE [c768637d6] 2026-07-02 15:06:12 +0900 Fix jsonpath's .decimal method to not throw a hard error for incorrect precision or scale (Ewan Young) --> -《機械翻訳》«Fix jsonpath's .decimal method to not throw a hard error for incorrect precision or scale » +《機械翻訳》jsonpath.decimalメソッドが誤ったハードまたはエラーに対して精度位取りをスローしないように修正した。 (Ewan Young) § § @@ -2450,7 +2514,7 @@ Branch: REL_17_STABLE [c768637d6] 2026-07-02 15:06:12 +0900 -《機械翻訳》«Silent mode should suppress these errors, but failed to.» +《機械翻訳》サイレントモードはこれらの誤りを抑制すべきであるが、できなかった。 @@ -2468,7 +2532,8 @@ Branch: REL_16_STABLE [60abb3c73] 2026-06-11 16:17:58 +0200 constructs have an argument that is of string category but lacks a cast to type text (Ayush Tiwari) --> -《機械翻訳》«Fix NULL-pointer crash when IS JSON or similar constructs have an argument that is of string category but lacks a cast to type text » +《機械翻訳》IS JSONまたは類似の構成に、NULLポインタのクラッシュはあるが、タイプテキストへの引数がない場合、文字列カテゴリを修正しました。 +キャスト (Ayush Tiwari) § @@ -2479,7 +2544,7 @@ Branch: REL_16_STABLE [60abb3c73] 2026-06-11 16:17:58 +0200 core PostgreSQL, but the problem is reachable with some extension types. --> -《機械翻訳》«There are no such data types in core PostgreSQL, but the problem is reachable with some extension types.» +《機械翻訳》コアにはそのようなデータタイプはありませんPostgreSQLただし、一部のextensionタイプでは問題が発生する可能性があります。 @@ -2496,7 +2561,7 @@ Branch: REL_17_STABLE [71cd10cd2] 2026-07-07 08:26:50 +0900 Ensure that SQL/JSON ON EMPTY / ON ERROR DEFAULT values are coerced to the correct typmod (Ewan Young) --> -《機械翻訳》«Ensure that SQL/JSON ON EMPTY / ON ERROR DEFAULT values are coerced to the correct typmod » +《機械翻訳》保証SQL/JSON ON EMPTY / ON ERROR DEFAULT値は正しいtypmodに強制されます。 (Ewan Young) § @@ -2507,7 +2572,7 @@ Branch: REL_17_STABLE [71cd10cd2] 2026-07-07 08:26:50 +0900 a numeric target column were not applied to the default value. --> -《機械翻訳》«For example, the declared precision and scale of a numeric target column were not applied to the default value.» +《機械翻訳》例の場合、宣言された精度と数値位取りカラムのターゲットはデフォルト値に適用されませんでした。 @@ -2527,7 +2592,7 @@ Branch: REL_14_STABLE [fec40878c] 2026-08-04 18:01:55 +1200 Avoid machine-dependent behavior when dividing the smallest possible money value by -1 (Andrey Rachitskiy) --> -《機械翻訳》«Avoid machine-dependent behavior when dividing the smallest possible money value by -1 » +《機械翻訳》可能な最小の金額値を-1で割るときは、マシンに依存する動作を避けてください。 (Andrey Rachitskiy) § @@ -2549,7 +2614,7 @@ Branch: REL_14_STABLE [dda622edc] 2026-08-02 16:49:18 -0400 Fix crash after out-of-memory failure partway through creation of a cache entry for a text search dictionary (Tom Lane) --> -《機械翻訳》«Fix crash after out-of-memory failure partway through creation of a cache entry for a text search dictionary » +《機械翻訳》クラッシュディクショナリのメモリエントリの作成中に、キャッシュ外エラーが発生した場合にテキストサーチを修正しました。 (Tom Lane) § @@ -2571,7 +2636,7 @@ Branch: REL_14_STABLE [cfc720ef4] 2026-08-02 13:22:39 -0400 Fix memory-safety bugs in processing of incorrect ispell/hunspell dictionary files (Andrey Rachitskiy) --> -《機械翻訳》«Fix memory-safety bugs in processing of incorrect ispell/hunspell dictionary files » +《機械翻訳》間違ったispell/hunspellディクショナリファイルを処理する際のメモリ安全に関するバグを修正しました。 (Andrey Rachitskiy) § @@ -2594,7 +2659,7 @@ Branch: REL_16_STABLE [cc3fe7e2a] 2026-07-03 18:01:00 +0300 Prevent access to other sessions' temporary tables (Jim Jones, Daniil Davydov, Alexander Korotkov) --> -《機械翻訳》«Prevent access to other sessions' temporary tables » +《機械翻訳》他のセッションのアクセステーブルへの一時的を禁止します。 (Jim Jones, Daniil Davydov, Alexander Korotkov) § § @@ -2605,7 +2670,7 @@ Branch: REL_16_STABLE [cc3fe7e2a] 2026-07-03 18:01:00 +0300 Some code paths failed to prevent this, leading to silently wrong (inconsistent) results. --> -《機械翻訳》«Some code paths failed to prevent this, leading to silently wrong (inconsistent) results.» +《機械翻訳》コード経路の中にはこれを防ぐことができず、静かに間違い(一貫性のない)結果をもたらした。 @@ -2620,7 +2685,7 @@ Branch: REL_18_STABLE [ed8050370] 2026-08-05 11:44:01 -0400 Prevent no empty local buffer available errors during temporary table access (Melanie Plageman) --> -《機械翻訳》«Prevent no empty local buffer available errors during temporary table access » +《機械翻訳》ローカルバッファアクセス時のエラー空の一時テーブルがないを防止します。 (Melanie Plageman) § @@ -2632,7 +2697,8 @@ Branch: REL_18_STABLE [ed8050370] 2026-08-05 11:44:01 -0400 of effective_io_concurrency could allow a single stream to use all the buffers, resulting in failure. --> -《機械翻訳》«Limit the number of local buffers that the read streaming mechanism is allowed to use. Previously, a large value of effective_io_concurrency could allow a single stream to use all the buffers, resulting in failure.» +《機械翻訳》読取りローカル・ストリーミングが使用できるメカニズム・バッファの数を制限します。 +以前は、ラージ値にeffective_io_concurrencyを指定すると、1つのストリームがすべてのバッファを使用できるため、エラーが発生していました。 @@ -2649,7 +2715,7 @@ Branch: REL_17_STABLE [288d4e83f] 2026-07-31 10:34:40 -0500 Fix the order in which autovacuum processes databases (Rustam Khamidullin) --> -《機械翻訳》«Fix the order in which autovacuum processes databases » +《機械翻訳》自動バキュームプロセスがデータベースに登録しているオーダーを修正します。 (Rustam Khamidullin) § @@ -2659,7 +2725,7 @@ Branch: REL_17_STABLE [288d4e83f] 2026-07-31 10:34:40 -0500 It was unintentionally processing databases from lowest to highest score, when it should be doing the reverse. --> -《機械翻訳》«It was unintentionally processing databases from lowest to highest score, when it should be doing the reverse.» +《機械翻訳》本来はその逆であるべきデータベースを、意図せずに最低スコアから最高スコアまで処理していました。 @@ -2679,7 +2745,7 @@ Branch: REL_18_STABLE [7c25cdb1e] 2026-08-07 10:06:41 -0400 in VACUUM's wraparound failsafe mode (Melanie Plageman) --> -《機械翻訳》«Restore full use of shared buffer pool in VACUUM's wraparound failsafe mode » +《機械翻訳》リストアVACUUM周回フェイルセーフモードでの共同バッファプールのフル活用 (Melanie Plageman) § § @@ -2694,7 +2760,10 @@ Branch: REL_18_STABLE [7c25cdb1e] 2026-08-07 10:06:41 -0400 to allow vacuuming to proceed as fast as possible. This behavior was accidentally broken during refactoring in v18; restore it. --> -《機械翻訳》«An ordinary VACUUM is limited to use just a few shared buffers, so as not to impinge too much on other processing. However, in failsafe mode we want to reclaim transaction IDs as quickly as possible, so that limit is supposed to be abandoned to allow vacuuming to proceed as fast as possible. This behavior was accidentally broken during refactoring in v18; restore it.» +《機械翻訳》通常のVACUUMは、他の処理にあまり影響を与えないように、いくつかの共有バッファのみを使用するように制限されています。 +ただし、フェイルセーフモードでは、できるだけ早くトランザクションIDを再要求したいので、この制限は放棄され、バキューム処理ができるだけ早く処理できるようになります。 +この動作は、v18でのリファクタリング中に誤って壊れました。 +リストアit。 @@ -2709,7 +2778,7 @@ Branch: REL_17_STABLE [8ad414831] 2026-06-08 15:29:21 +0900 -《機械翻訳》«Fix memory leak in parallel vacuum worker processes » +《機械翻訳》パラレルバキュームワーカープロセスでメモリリークを固定します。 (Baji Shaik) § @@ -2719,7 +2788,7 @@ Branch: REL_17_STABLE [8ad414831] 2026-06-08 15:29:21 +0900 Progress reports from a parallel worker leaked about 1kB per report, with the waste accumulating for the life of the worker process. --> -《機械翻訳》«Progress reports from a parallel worker leaked about 1kB per report, with the waste accumulating for the life of the worker process.» +《機械翻訳》パラレルワーカーからの経過報告は、レポートごとに約1つのキロバイトが漏洩し、ワーカープロセスの存続期間中に廃棄物が蓄積した。 @@ -2739,7 +2808,7 @@ Branch: REL_14_STABLE [15fd7a3e2] 2026-07-28 10:56:39 +0200 Honor query cancel and vacuum delay during GIN index posting-tree cleanup (Paul Kim, Alexander Korotkov) --> -《機械翻訳》«Honor query cancel and vacuum delay during GIN index posting-tree cleanup » +《機械翻訳》GINキャンセル赴任中に問い合わせインデックスとバキューム遅延に敬意を表します-ツリークリーンアップ。 (Paul Kim, Alexander Korotkov) § @@ -2750,7 +2819,7 @@ Branch: REL_14_STABLE [15fd7a3e2] 2026-07-28 10:56:39 +0200 missed check could allow vacuum to run for a long time before noticing an interrupt. --> -《機械翻訳》«The posting tree for a common value can be large, so that this missed check could allow vacuum to run for a long time before noticing an interrupt.» +《機械翻訳》共通値のための提示ツリーは、ラージとすることができ、したがって、この逃したチェックは、バキュームが中断に気づいて長い間前を実行することを可能にすることができる。 @@ -2770,7 +2839,7 @@ Branch: REL_14_STABLE [e4ad22eb0] 2026-07-17 15:52:56 -0400 Fix possible mis-decoding of index tuples during GiST and SP-GiST index-only scans (Peter Geoghegan) --> -《機械翻訳》«Fix possible mis-decoding of index tuples during GiST and SP-GiST index-only scans » +《機械翻訳》インデックスおよびSP-GiST GiSTのみのスキャン中に発生する可能性があったインデックスタプルのデコードミスが修正されました。 (Peter Geoghegan) § @@ -2782,7 +2851,8 @@ Branch: REL_14_STABLE [e4ad22eb0] 2026-07-17 15:52:56 -0400 it could only fail if the range column were not the first index column. --> -《機械翻訳》«This error could lead to emitting corrupted data from an index-only scan plan. The only affected core opclass is GiST's range_ops, and it could only fail if the range column were not the first index column.» +《機械翻訳》このエラーは、インデックスのみのスキャンプランからの汚染されたデータの排出につながる可能性があります。 +影響を受ける唯一のコアopclassはGiSTのレンジ_opsであり、レンジカラムが最初のインデックスカラムでない場合にのみ失敗します。 @@ -2800,7 +2870,7 @@ Branch: REL_16_STABLE [0fd5595aa] 2026-07-15 15:58:15 -0400 Ensure that the new last block of a bulk-extended table is added to its free space map promptly (Jingtang Zhang) --> -《機械翻訳》«Ensure that the new last block of a bulk-extended table is added to its free space map promptly » +《機械翻訳》バルク-拡張テーブルの新たな最後のブロックがその空き領域マップに速やかに追加される保証。 (Jingtang Zhang) § @@ -2812,7 +2882,8 @@ Branch: REL_16_STABLE [0fd5595aa] 2026-07-15 15:58:15 -0400 eventually get corrected by vacuum, but meanwhile the space wouldn't be used. --> -《機械翻訳》«An off-by-one error caused the last block of a multi-block table extension to not be marked as free in the map. This would eventually get corrected by vacuum, but meanwhile the space wouldn't be used.» +《機械翻訳》マルチ-ブロックテーブルextensionの最後のブロックがマップでフリーと表示されなかったのは、オフごとのエラーが原因でした。 +これは最終的にバキュームによって修正されますが、その間スペースは使用されませんでした。 @@ -2828,7 +2899,8 @@ Branch: REL_17_STABLE [011eedcdc] 2026-06-22 18:03:23 -0400 Avoid possible double-free or infinite error recovery loop in resource cleanup during transaction abort (Tom Lane) --> -《機械翻訳》«Avoid possible double-free or infinite error recovery loop in resource cleanup during transaction abort » +《機械翻訳》トランザクション中断中のリソースフリーでは、二重無限またはエラーリカバリループの可能性を避けてください。 +クリーンアップ (Tom Lane) § @@ -2849,7 +2921,7 @@ Branch: REL_14_STABLE [4b3bc6b71] 2026-06-19 12:52:00 -0400 When creating directories, tolerate concurrent creation of the same directory (Andrew Dunstan, Tom Lane) --> -《機械翻訳》«When creating directories, tolerate concurrent creation of the same directory » +《機械翻訳》ディレクトリを作成する場合、同じディレクトリの同時作成を許容します。 (Andrew Dunstan, Tom Lane) § @@ -2866,7 +2938,7 @@ Branch: REL_18_STABLE [e9692de1d] 2026-06-19 15:26:51 +1200 Fix JIT-compiled tuple deconstruction code to account correctly for virtual generated columns (David Rowley) --> -《機械翻訳》«Fix JIT-compiled tuple deconstruction code to account correctly for virtual generated columns » +《機械翻訳》仮想的に生成された列に対して、JITコンパイルされたタプル分解コードがアカウントに正しく修正されました。 (David Rowley) § @@ -2893,7 +2965,7 @@ Branch: REL_14_STABLE [36b6ed260] 2026-05-27 18:37:48 +0300 Prevent creation of dangling object dependencies by acquiring a shared lock on any object being depended on (Bertrand Drouvot) --> -《機械翻訳》«Prevent creation of dangling object dependencies by acquiring a shared lock on any object being depended on » +《機械翻訳》依存しているオブジェクトの共有ロックを取得することで、ダングリングオブジェクトの依存関係が作成されないようにします。 (Bertrand Drouvot) § § @@ -2909,7 +2981,9 @@ Branch: REL_14_STABLE [36b6ed260] 2026-05-27 18:37:48 +0300 leaving an invalid function definition behind. Now, one transaction or the other will fail. --> -《機械翻訳》«The shared lock will conflict with any attempt to drop the depended-on object, eliminating the race condition that formerly existed. For example, if one session drops a schema (that appears empty to it) concurrently with some other session creating a function in that schema, previously both transactions could commit, leaving an invalid function definition behind. Now, one transaction or the other will fail.» +《機械翻訳》共有ロックは、以前に存在していたオブジェクトを削除して、依存していた競合条件を削除しようとする試みをコンフリクトします。 +例の場合、あるセッションが(空に見える)スキーマを削除すると同時に、他のセッションがそのスキーマに関数を作成すると、以前は両方の取引がコミットする可能性があり、無効な関数定義が残されていました。 +これで、どちらかのトランザクションが失敗します。 @@ -2930,7 +3004,7 @@ Branch: REL_14_STABLE [2fc3e1b44] 2026-07-25 12:01:24 -0400 for SERIALIZABLE isolation mode (Peter Geoghegan) --> -《機械翻訳》«Fix race condition in conflict detection for SERIALIZABLE isolation mode » +《機械翻訳》SERIALIZABLE隔離競合条件のコンフリクト検出にモードを固定する。 (Peter Geoghegan) § @@ -2941,7 +3015,7 @@ Branch: REL_14_STABLE [2fc3e1b44] 2026-07-25 12:01:24 -0400 index, allowing failure of serializability due to improperly allowing conflicting transactions to commit. --> -《機械翻訳》«A conflict could be missed when examining an initially-empty btree index, allowing failure of serializability due to improperly allowing conflicting transactions to commit.» +《機械翻訳》最初は空であったb-treeコンフリクトを検査する際にインデックスが見落とされる可能性があり、コミットへの競合するトランザクションを不適切に許可することにより、直列化可能性の失敗を可能にする。 @@ -2958,7 +3032,7 @@ Branch: REL_15_STABLE [159324a73] 2026-05-27 16:26:08 -0700 -《機械翻訳》«Fix race condition in ProcSignalBarrier code » +《機械翻訳》ProcSignalBarrierコードの競合条件を修正します。 (Masahiko Sawada) § @@ -2970,7 +3044,7 @@ Branch: REL_15_STABLE [159324a73] 2026-05-27 16:26:08 -0700 PID nnnn to accept ProcSignalBarrier. --> -《機械翻訳》«This error could result in processes getting stuck, typically after reporting still waiting for backend with PID nnnn to accept ProcSignalBarrier.» +《機械翻訳》このエラーにより、通常はレポート後にプロセスが停止する可能性がありますnnnn ProcSignalBarrierを受け入れるためにPIDでバックエンドを待機しています。 @@ -2995,7 +3069,7 @@ Branch: REL_14_STABLE [db4d12fc9] 2026-05-27 17:20:00 +0900 Fix race conditions when a set of processes that belong to the same lock group exit at the same time (Vlad Lesin) --> -《機械翻訳》«Fix race conditions when a set of processes that belong to the same lock group exit at the same time » +《機械翻訳》同じロックグループに属する一連のプロセスが同時に終了する場合の競合状態を修正した。 (Vlad Lesin) § § @@ -3008,7 +3082,10 @@ Branch: REL_14_STABLE [db4d12fc9] 2026-05-27 17:20:00 +0900 arise in regular parallel query, since the leader won't exit before seeing its workers finish; but some extensions reach the problem. --> -《機械翻訳》«These errors could lead to PANIC aborts, with messages such as latch already owned. The issue does not normally arise in regular parallel query, since the leader won't exit before seeing its workers finish; but some extensions reach the problem.» +《機械翻訳》これらのエラーは、パニックはすでに所有されていますのようなメッセージを表示して、ラッチの中断につながる可能性があります。 +この問題は通常のパラレル問い合わせでは発生しません。 +なぜなら、リーダーはワーカーが終了するのを見て前を出ることはないからです。 +しかし、いくつかの拡張はこの問題に到達します。 @@ -3033,7 +3110,7 @@ Branch: REL_17_STABLE [067213430] 2026-07-15 17:43:38 -0400 Fix WAL logging of operations that clear bits in tables' visibility maps (Melanie Plageman, Andres Freund) --> -《機械翻訳》«Fix WAL logging of operations that clear bits in tables' visibility maps » +《機械翻訳》ロギングがテーブルの可視マップに割り込む操作のWALクリアを修正しました。 (Melanie Plageman, Andres Freund) § § @@ -3048,7 +3125,9 @@ Branch: REL_17_STABLE [067213430] 2026-07-15 17:43:38 -0400 torn page writes to go uncorrected. This could lead to misbehavior later, such as wrong results from index-only scans. --> -《機械翻訳》«Such VM changes were missed by the WAL summarizer, potentially leading to incorrect incremental backups. We also failed to log full-page images of such VM pages when needed, potentially allowing torn page writes to go uncorrected. This could lead to misbehavior later, such as wrong results from index-only scans.» +《機械翻訳》このようなVMの変更はWALサマライザによって見逃され、不正な増分バックアップを引き起こす可能性がありました。 +また、必要なときにこのようなVMページのフルページイメージをログすることに失敗し、引き裂かれたページ書き込みが修正されない可能性がありました。 +これは、インデックスのみのスキャンからの間違い結果など、後で不正な動作を引き起こす可能性がありました。 @@ -3069,7 +3148,7 @@ Branch: REL_17_STABLE [d28cdf46e] 2026-07-22 08:49:00 -0400 Prevent WAL summarizer process from getting stuck at a timeline switch (Robert Haas) --> -《機械翻訳》«Prevent WAL summarizer process from getting stuck at a timeline switch » +《機械翻訳》WALサマライザプロセスがタイムラインスイッチで立ち往生するのを防ぎます。 (Robert Haas) § § @@ -3094,7 +3173,7 @@ Branch: REL_16_STABLE [d9b49e5b4] 2026-06-12 11:44:19 +0900 Fix race with timeline selection in logical decoding during standby promotion (Bertrand Drouvot) --> -《機械翻訳》«Fix race with timeline selection in logical decoding during standby promotion » +《機械翻訳》スタンバイのプロモーション中にロジカルデコーディングのタイムライン選択とのレースを修正しました。 (Bertrand Drouvot) § § @@ -3107,7 +3186,8 @@ Branch: REL_16_STABLE [d9b49e5b4] 2026-06-12 11:44:19 +0900 error. A repeat attempt would succeed, so there was no permanent problem but there was an availability hazard. --> -《機械翻訳》«Logical decoding being performed on the standby could fail with a requested WAL segment has already been removed error. A repeat attempt would succeed, so there was no permanent problem but there was an availability hazard.» +《機械翻訳》スタンバイで実行されるロジカルデコーディングは、要求されたWALセグメントが既に削除されているエラーで失敗する可能性がありました。 +再試行は成功するため、永続的な問題はありませんでしたが、可用性の危険がありました。 @@ -3126,7 +3206,7 @@ Branch: REL_14_STABLE [e18b77153] 2026-05-23 08:10:18 +0900 Avoid exposing a WAL receiver's full connection string during timeline jumps (Chao Li) --> -《機械翻訳》«Avoid exposing a WAL receiver's full connection string during timeline jumps » +《機械翻訳》タイムラインジャンプ中に、WALレシーバのコネクション文字列全体を露出させないようにします。 (Chao Li) § @@ -3138,7 +3218,8 @@ Branch: REL_14_STABLE [e18b77153] 2026-05-23 08:10:18 +0900 But it transiently showed the full string when we re-use an existing WAL receiver. --> -《機械翻訳》«The pg_stat_wal_receiver view should show a sanitized version of the connection string, without sensitive data. But it transiently showed the full string when we re-use an existing WAL receiver.» +《機械翻訳》pg_stat_wal_receiverビューはコネクション文字列の消毒されたバージョンを示し、敏感なデータは示さないはずです。 +しかし、既存のWALレシーバを再利用するときに一時的に完全な文字列を示しました。 @@ -3158,7 +3239,7 @@ Branch: REL_14_STABLE [510a05f07] 2026-05-16 18:01:46 -0700 of columns in tuples received during logical replication (Varik Matevosyan) --> -《機械翻訳》«Use run-time checks, not just Asserts, to verify the correct number of columns in tuples received during logical replication » +《機械翻訳》アサート中に受け取ったタプルの正しい列数を検証するために、論理レプリケーションだけではなく、ランタイム検査を使用してください。 (Varik Matevosyan) § @@ -3169,7 +3250,8 @@ Branch: REL_14_STABLE [510a05f07] 2026-05-16 18:01:46 -0700 columns. While we could not find a scenario in which this would have serious ill effects, extra caution seems warranted. --> -《機械翻訳》«A malicious or buggy publisher could send inconsistent numbers of columns. While we could not find a scenario in which this would have serious ill effects, extra caution seems warranted.» +《機械翻訳》悪意のあるパブリッシャーやバグのあるシナリオは、矛盾した数の列を送信する可能性があります。 +これが深刻な悪影響を及ぼす地域は見つかりませんでしたが、特別な注意が必要と思われます。 @@ -3188,7 +3270,7 @@ Branch: REL_14_STABLE [2a00840e8] 2026-06-15 15:35:37 -0400 Clean up quoting of string parameters within constructed replication commands (Tom Lane) --> -《機械翻訳》«Clean up quoting of string parameters within constructed replication commands » +《機械翻訳》建設された文字列内のレプリケーションコマンドパラメータの見積もりをクリーンアップします。 (Tom Lane) § @@ -3205,7 +3287,10 @@ Branch: REL_14_STABLE [2a00840e8] 2026-06-15 15:35:37 -0400 users and there is no reason for them to use a slot name coming from an untrustworthy source. --> -《機械翻訳》«Various places that generate replication commands were not being adequately careful about quoting replication slot names and other parameters that need to be inserted into those commands. This could result in unexpected syntax errors in those commands. In principle, a crafted replication slot name could result in SQL injection; but such a scenario seems very unlikely to occur in practice, since replication operations can only be invoked by highly-privileged users and there is no reason for them to use a slot name coming from an untrustworthy source.» +《機械翻訳》レプリケーションコマンドを生成する様々な場所で、これらのコマンドに挿入する必要があるレプリケーションスロット名やその他のパラメータの引用に十分な注意が払われていませんでした。 +これにより、これらのコマンドで予期しない構文エラーが発生する可能性がありました。 +原則的には、巧妙に細工されたレプリケーションスロット名前はSQLインジェクションを引き起こす可能性がありますが、実際にはそのようなシナリオは非常に起こりにくいと思われます。 +なぜなら、レプリケーション操作は高度な権限を持つユーザのみが呼び出すことができ、信頼できないソースからのスロット名前を使用する理由はないからです。 @@ -3224,7 +3309,7 @@ Branch: REL_14_STABLE [c2d34db0a] 2026-07-28 12:33:50 -0700 -《機械翻訳》«Fix logical decoding of empty prepared transactions » +《機械翻訳》空の準備されたトランザクションのロジカルデコーディングを修正しました。 (Masahiko Sawada) § @@ -3237,7 +3322,8 @@ Branch: REL_14_STABLE [c2d34db0a] 2026-07-28 12:33:50 -0700 built-in subscriber this breaks replication, and other plugins will probably not like it either. --> -《機械翻訳》«A prepared transaction that did not cause any decodable updates could result in sending COMMIT/ROLLBACK PREPARED to the output plugin with no preceding PREPARE. For the built-in subscriber this breaks replication, and other plugins will probably not like it either.» +《機械翻訳》デコード可能な更新を引き起こさなかったプリペアードトランザクションは、先行するPREPAREがないCOMMIT/ROLLBACK PREPAREDを出力プラグインに送信する結果になる可能性があります。 +組み込みのサブスクライバーでは、これはレプリケーションを壊し、他のプラグインもおそらくそれを好まないでしょう。 @@ -3256,7 +3342,7 @@ Branch: REL_15_STABLE [d2980067b] 2026-06-30 08:52:50 +0900 Fix corruption of unlogged sequences after standby promotion (Fujii Masao) --> -《機械翻訳》«Fix corruption of unlogged sequences after standby promotion » +《機械翻訳》スタンバイ昇格後のログされないシーケンスの破損を修正しました。 (Fujii Masao) § @@ -3268,7 +3354,7 @@ Branch: REL_15_STABLE [d2980067b] 2026-06-30 08:52:50 +0900 standby could fail with bad magic number in sequence or related errors. --> -《機械翻訳》«Previously, if an unlogged sequence was created on the primary and replicated to a standby, accessing the sequence after promoting the standby could fail with bad magic number in sequence or related errors.» +《機械翻訳》以前は、ログに記録されていないシーケンスがプライマリで作成され、スタンバイに複製された場合、スタンバイをプロモートした後にシーケンスをアクセスすると、シーケンスのマジックナンバーが悪いまたは関連するエラーで失敗することがありました。 @@ -3288,7 +3374,7 @@ Branch: REL_14_STABLE [5fb3c6389] 2026-07-29 17:15:45 +0200 Fix cascading standby reconnect failure after archive fallback (Marco Nenciarini) --> -《機械翻訳》«Fix cascading standby reconnect failure after archive fallback » +《機械翻訳》カスケードスタンバイ代替後のアーカイブ再接続障害を修正しました。 (Marco Nenciarini) § @@ -3299,7 +3385,7 @@ Branch: REL_14_STABLE [5fb3c6389] 2026-07-29 17:15:45 +0200 with requested starting point ... is ahead of the WAL flush position after falling back to archive recovery. --> -《機械翻訳》«A cascading standby could fail to reconnect to its upstream standby with requested starting point ... is ahead of the WAL flush position after falling back to archive recovery.» +《機械翻訳》カスケードスタンバイは、アーカイブスタンバイにフォールバックした後、要求された開始点が.WALフラッシュ位置の前にありますで上流リカバリへの再接続に失敗する可能性があります。 @@ -3315,7 +3401,7 @@ Branch: REL_18_STABLE [311e66df9] 2026-08-08 00:10:06 +0900 Prevent accepting hot-standby connections before WAL replay has reached a consistent database state (Nikhil Sontakke) --> -《機械翻訳》«Prevent accepting hot-standby connections before WAL replay has reached a consistent database state » +《機械翻訳》ホット-スタンバイ間の接続を許可しない前WALリプレイが一貫したデータベース状態になりました。 (Nikhil Sontakke) § @@ -3334,7 +3420,7 @@ Branch: REL_17_STABLE [4a375527a] 2026-05-27 02:28:49 +0300 pg_database.dathasloginevt locally on a standby server (Ayush Tiwari) --> -《機械翻訳》«Do not try to clear pg_database.dathasloginevt locally on a standby server » +《機械翻訳》トライからクリアpg_データベースには移動しないでくださいdathasloginevtスタンバイサーバでローカルに移動します。 (Ayush Tiwari) § @@ -3346,7 +3432,8 @@ Branch: REL_17_STABLE [4a375527a] 2026-05-27 02:28:49 +0300 and there's no need anyway since replay of the primary's database change will soon fix it. --> -《機械翻訳》«Event trigger cleanup tried to perform that action on standby servers as well as the primary. That can't work on a standby, and there's no need anyway since replay of the primary's database change will soon fix it.» +《機械翻訳》イベントトリガクリーンアップはプライマリだけでなくスタンバイサーバでもその行動を実行しようとしました。 +それはスタンバイでは機能しませんし、プライマリのデータベース変更のリプレイがすぐにそれを修正するので、とにかくその必要はありません。 @@ -3362,7 +3449,7 @@ Branch: REL_17_STABLE [ea834d747] 2026-06-18 09:35:53 +0530 Avoid race condition while dropping obsolete replication slots (Xuneng Zhou) --> -《機械翻訳》«Avoid race condition while dropping obsolete replication slots » +《機械翻訳》古い競合条件スロットを削除するときは、レプリケーションを使用しないでください。 (Xuneng Zhou) § @@ -3372,7 +3459,7 @@ Branch: REL_17_STABLE [ea834d747] 2026-06-18 09:35:53 +0530 An incorrect unlock and log message could occur if another session immediately re-used the dropped slot's shared-memory entry. --> -《機械翻訳》«An incorrect unlock and log message could occur if another session immediately re-used the dropped slot's shared-memory entry.» +《機械翻訳》誤ったロック解除およびログメッセージは、別のセッションが、ドロップされたスロットの共有メモリエントリを直ちに再利用した場合に発生する可能性がある。 @@ -3391,7 +3478,7 @@ Branch: REL_14_STABLE [968c50845] 2026-06-03 18:47:52 +0900 Avoid race condition while dropping ephemeral replication slots (Zhijie Hou) --> -《機械翻訳》«Avoid race condition while dropping ephemeral replication slots » +《機械翻訳》エフェメラルレプリケーションスロットをドロップするときは、競合条件を避けてください。 (Zhijie Hou) § @@ -3404,7 +3491,9 @@ Branch: REL_14_STABLE [968c50845] 2026-06-03 18:47:52 +0900 dropped slot's shared-memory entry. Skip those updates in the case of an ephemeral slot. --> -《機械翻訳》«The slot-releasing code performed some additional updates to the replication slot's shared-memory entry after releasing the slot. This is unsafe since another session could immediately re-use the dropped slot's shared-memory entry. Skip those updates in the case of an ephemeral slot.» +《機械翻訳》スロットをリリースするコードは、レプリケーションスロットをリリースした後に、スロットの共有メモリエントリに対していくつかの追加更新を実行しました。 +これは、削除されたスロットの共有メモリエントリを別のセッションがすぐに再利用できるため、安全ではありません。 +スキップは、一時的なスロットのケースでこれらの更新を行います。 @@ -3423,7 +3512,7 @@ Branch: REL_14_STABLE [e3c4e3746] 2026-05-13 11:46:26 +0900 Fix stale progress reports during logical replication table synchronization (Shinya Kato) --> -《機械翻訳》«Fix stale progress reports during logical replication table synchronization » +《機械翻訳》論理レプリケーションテーブルの同期化中に古い進行状況レポートを修正しました。 (Shinya Kato) § @@ -3436,7 +3525,8 @@ Branch: REL_14_STABLE [e3c4e3746] 2026-05-13 11:46:26 +0900 data copy had finished. The stale entry remained visible until synchronization caught up with the publisher. --> -《機械翻訳》«Previously, the pg_stat_progress_copy view in the subscriber would continue to show the initial COPY operation as active even after the data copy had finished. The stale entry remained visible until synchronization caught up with the publisher.» +《機械翻訳》これまでは、サブスクライバーのpg_stat_progress_コピービューは、データコピーが終了した後も最初のCOPYオペレーションをアクティブとして表示し続けていました。 +古いエントリは、同期がパブリッシャーに追いつくまで可視のままでした。 @@ -3458,7 +3548,7 @@ Branch: REL_15_STABLE [a8fb98b7b] 2026-07-06 09:48:06 +0900 -《機械翻訳》«Clear base backup progress on backup failure » +《機械翻訳》バックアップの失敗に関するクリアベースバックアップの進展。 (Chao Li) § § @@ -3472,7 +3562,7 @@ Branch: REL_15_STABLE [a8fb98b7b] 2026-07-06 09:48:06 +0900 disconnected. pg_basebackup normally disconnects immediately, but other clients might not. --> -《機械翻訳》«Previously the pg_stat_progress_basebackup view would continue to show a stale progress entry after a failure, until the replication client disconnected. pg_basebackup normally disconnects immediately, but other clients might not.» +《機械翻訳》以前は、pg_stat_progress_basebackupビューは、障害発生後、エントリクライアントが切断されるまで、古い進行状況レプリケーションを表示し続けましたpg_basebackup通常はすぐに切断されますが、他のクライアントは切断されない場合があります。 @@ -3499,7 +3589,7 @@ Branch: REL_15_STABLE [9e4771825] 2026-06-23 07:59:03 +0900 when track_functions is enabled (Sami Imseih, Michael Paquier) --> -《機械翻訳》«Fix possible PANIC due to concurrent drop of pgstats entries when track_functions is enabled » +《機械翻訳》track_functionsが有効な場合にpgstatsエントリが同時に削除されることで発生する可能性があったパニックを修正しました。 (Sami Imseih, Michael Paquier) § § @@ -3522,7 +3612,7 @@ Branch: REL_15_STABLE [10e20e59e] 2026-08-07 14:23:39 +0900 Clean up broken local pgstats entry after failing to obtain space for the corresponding shared hashtable entry (Niall Newman) --> -《機械翻訳》«Clean up broken local pgstats entry after failing to obtain space for the corresponding shared hashtable entry » +《機械翻訳》対応する共有ハッシュテーブルローカル用のエントリの取得に失敗した後、壊れたスペースpgstatsエントリをクリーンアップします。 (Niall Newman) § @@ -3532,7 +3622,7 @@ Branch: REL_15_STABLE [10e20e59e] 2026-08-07 14:23:39 +0900 Failure to do this led to a null-pointer dereference the next time the local entry was used. --> -《機械翻訳》«Failure to do this led to a null-pointer dereference the next time the local entry was used.» +《機械翻訳》これを行わなかった場合、次にローカルエントリが使用されたときにNULL-ポインタ間の逆参照が発生しました。 @@ -3547,7 +3637,7 @@ Branch: REL_18_STABLE [13f940b4b] 2026-06-17 16:05:37 +0900 Avoid recording incorrect I/O operation statistics after a failed read or write (Bertrand Drouvot) --> -《機械翻訳》«Avoid recording incorrect I/O operation statistics after a failed read or write » +《機械翻訳》読み取りまたは書き込みに失敗した後に、誤った入出力オペレーション統計処理を記録しないようにしてください。 (Bertrand Drouvot) § @@ -3569,7 +3659,8 @@ Branch: REL_14_STABLE [e520ad34b] 2026-06-24 09:17:36 +0900 dereference crash when working with an invalid PostgreSQL::InServer::ARRAY object (Xing Guo) --> -《機械翻訳》«In PL/Perl, avoid NULL pointer dereference crash when working with an invalid PostgreSQL::InServer::ARRAY object » +《機械翻訳》では、無効なヌルポインタクラッシュ::InServer::PostgreSQLを操作するときに配列がオブジェクトを逆参照しないようにします。 +PL/Perl (Xing Guo) § @@ -3591,7 +3682,7 @@ Branch: REL_14_STABLE [309dc4526] 2026-06-29 14:21:15 +0900 In PL/Python, properly check for errors when working with sequence and mapping objects (Richard Guo) --> -《機械翻訳》«In PL/Python, properly check for errors when working with sequence and mapping objects » +《機械翻訳》PL/Pythonでは、シーケンスオブジェクトとマッピングオブジェクトを使用するときのエラーが正しくチェックされるようになりました。 (Richard Guo) § @@ -3601,7 +3692,7 @@ Branch: REL_14_STABLE [309dc4526] 2026-06-29 14:21:15 +0900 Previously, a broken object or an unhandled exception could result in a NULL pointer dereference crash. --> -《機械翻訳》«Previously, a broken object or an unhandled exception could result in a NULL pointer dereference crash.» +《機械翻訳》以前は、壊れたオブジェクトまたは処理されない例外により、ヌルポインタのクラッシュの逆参照が発生する可能性があった。 @@ -3643,7 +3734,7 @@ Branch: REL_14_STABLE [7532f2117] 2026-07-09 18:36:41 +0300 from the SSL or GSS decryption buffer during pqReadData() (Jacob Champion) --> -《機械翻訳》«In libpq, always drain all pending bytes from the SSL or GSS decryption buffer during pqReadData() » +《機械翻訳》libpqでは、pqReadData()の間にSSLまたはGSS復号化バッファからすべての保留バイトを常に排出します。 (Jacob Champion) § § @@ -3657,7 +3748,7 @@ Branch: REL_14_STABLE [7532f2117] 2026-07-09 18:36:41 +0300 calling application waits for more data to arrive on the socket, but actually all the data has already arrived. --> -《機械翻訳》«This avoids edge cases where libpq or its calling application waits for more data to arrive on the socket, but actually all the data has already arrived.» +《機械翻訳》これにより、libpqまたはその呼び出し元のアプリケーションがソケットにさらにデータが到着するのを待っているが、実際にはすべてのデータがすでに到着しているというエッジケースが回避される。 @@ -3673,7 +3764,7 @@ Branch: REL_18_STABLE [6b46a5d1b] 2026-08-04 17:05:44 +0900 Improve libpq's handling of out-of-memory conditions (Anthonin Bonnefoy) --> -《機械翻訳》«Improve libpq's handling of out-of-memory conditions » +《機械翻訳》改善libpqメモリ外の状況のハンドリング。 (Anthonin Bonnefoy) § @@ -3692,7 +3783,7 @@ Branch: REL_18_STABLE [dd5eca055] 2026-07-03 15:00:00 +0300 new-style BackendKeyData and CancelRequest messages correctly (Anthonin Bonnefoy) --> -《機械翻訳》«Fix libpq's trace facility to print new-style BackendKeyData and CancelRequest messages correctly » +《機械翻訳》libpqのトレース機能をプリントの新しいスタイルBackendKeyDataメッセージとCancelRequestメッセージに正しく修正した。 (Anthonin Bonnefoy) § @@ -3713,7 +3804,7 @@ Branch: REL_14_STABLE [1b79c8d1a] 2026-06-15 11:38:40 +0300 Allow libpq to accept ParameterDescription messages exceeding 30000 bytes (Ning Sun) --> -《機械翻訳》«Allow libpq to accept ParameterDescription messages exceeding 30000 bytes » +《機械翻訳》Allow libpq 30000バイトを超えるParameterDescriptionメッセージを受け入れる。 (Ning Sun) § @@ -3725,7 +3816,8 @@ Branch: REL_14_STABLE [1b79c8d1a] 2026-06-15 11:38:40 +0300 could be long. The limit resulted in failure for prepared queries having more than 7498 parameters, which is unlikely but supported. --> -《機械翻訳》«Previously, this message type was not among those that libpq's validity heuristics believed could be long. The limit resulted in failure for prepared queries having more than 7498 parameters, which is unlikely but supported.» +《機械翻訳》以前は、このメッセージタイプは、libpq有効性のヒューリスティックスで長いと考えられていたものの中には含まれていなかった。 +この制限により、7498を超えるパラメータを持つプリペアド問い合わせで失敗したが、これは起こりそうにないがサポートされている。 @@ -3740,7 +3832,7 @@ Branch: REL_18_STABLE [917fdbc63] 2026-06-25 16:58:29 -0400 Fix null-pointer crash in ecpg compiler (Jehan-Guillaume de Rorthais) --> -《機械翻訳》«Fix null-pointer crash in ecpg compiler » +《機械翻訳》NULL-ポインタクラッシュをecpgコンパイラに固定する。 (Jehan-Guillaume de Rorthais) § @@ -3751,7 +3843,7 @@ Branch: REL_18_STABLE [917fdbc63] 2026-06-25 16:58:29 -0400 a DECLARE section containing a union nested inside a struct. --> -《機械翻訳》«ecpg failed on a DECLARE section containing a union nested inside a struct.» +《機械翻訳》構造体内にネストされたセクションを含むDECLAREユニオンでecpgが失敗しました。 @@ -3771,7 +3863,7 @@ Branch: REL_14_STABLE [9e8fd9f7a] 2026-06-08 17:14:20 +0900 in ecpg's GET/SET DESCRIPTOR statements (Masashi Kamura) --> -《機械翻訳》«Reject multiple descriptor header items in ecpg's GET/SET DESCRIPTOR statements » +《機械翻訳》ecpgGET/SET DESCRIPTORステートメント内のマルチプルディスクリプタヘッダ項目を拒否します。 (Masashi Kamura) § @@ -3782,7 +3874,8 @@ Branch: REL_14_STABLE [9e8fd9f7a] 2026-06-08 17:14:20 +0900 generated. Adjust the grammar and the documentation to allow only one header item. --> -《機械翻訳》«Previously the grammar allowed this syntax, but broken C code was generated. Adjust the grammar and the documentation to allow only one header item.» +《機械翻訳》以前は、文法はこの構文を許可していましたが、壊れたCコードが生成されました。 +文法と文書を調整して、ヘッダアイテムを1つだけ許可するようにしてください。 @@ -3797,7 +3890,7 @@ Branch: REL_18_STABLE [1e9bc4074] 2026-06-03 08:58:29 +0900 Fix issues with deferred errors in pipeline mode in psql (Michael Paquier) --> -《機械翻訳》«Fix issues with deferred errors in pipeline mode in psql » +《機械翻訳》psqlで、パイプラインモードの遅延エラーに関する問題を修正しました。 (Michael Paquier) § @@ -3809,7 +3902,7 @@ Branch: REL_18_STABLE [1e9bc4074] 2026-06-03 08:58:29 +0900 error in response to a Sync message, such as a deferred constraint violation. --> -《機械翻訳》«psql could get stuck or suffer an assertion failure in some scenarios where the server reports an error in response to a Sync message, such as a deferred constraint violation.» +《機械翻訳》psql遅延のアサーション違反など、サーバが回答のエラーを同期メッセージに報告する一部のシナリオでは、スタックまたは制約障害が発生する可能性があります。 @@ -3828,7 +3921,7 @@ Branch: REL_14_STABLE [a4ca91ea1] 2026-06-08 14:38:01 +0900 Make line widths match in psql's expanded aligned output format (Pavel Stehule) --> -《機械翻訳》«Make line widths match in psql's expanded aligned output format » +《機械翻訳》make直線幅マッチin psqlの拡張に位置合わせされた出力フォーマット。 (Pavel Stehule) § @@ -3838,7 +3931,7 @@ Branch: REL_14_STABLE [a4ca91ea1] 2026-06-08 14:38:01 +0900 When the table's data rows are narrower than the record header lines, widen the data rows to match the headers, avoiding unsightly output. --> -《機械翻訳》«When the table's data rows are narrower than the record header lines, widen the data rows to match the headers, avoiding unsightly output.» +《機械翻訳》テーブルのデータ行がレコードのヘッダ行よりも狭い場合は、データ行をヘッダのマッチまで広げて、見苦しい出力を回避します。 @@ -3855,7 +3948,7 @@ Branch: REL_18_STABLE [e0c641ebb] 2026-05-18 08:33:36 -0700 variable WATCH_INTERVAL (Sven Klemm, Daniel Gustafsson) --> -《機械翻訳》«Enforce the intended upper limit for psql's special variable WATCH_INTERVAL » +《機械翻訳》psqlの特別変数の意図された上限を実施するWATCH_INTERVAL。 (Sven Klemm, Daniel Gustafsson) § @@ -3865,7 +3958,7 @@ Branch: REL_18_STABLE [e0c641ebb] 2026-05-18 08:33:36 -0700 If a too-large value was given, psql reported an error but applied the setting anyway. --> -《機械翻訳》«If a too-large value was given, psql reported an error but applied the setting anyway.» +《機械翻訳》too-ラージ値が指定された場合、psqlはエラーを報告しましたが、設定を適用しました。 @@ -3885,7 +3978,7 @@ Branch: REL_14_STABLE [4e19081da] 2026-07-25 19:11:34 +0900 Fix psql's privilege check for showing database size in \l+ (Christoph Berg) --> -《機械翻訳》«Fix psql's privilege check for showing database size in \l+ » +《機械翻訳》データベースサイズを見せたフィックスpsqlの権限チェック\l+。 (Christoph Berg) § @@ -3899,7 +3992,9 @@ Branch: REL_14_STABLE [4e19081da] 2026-07-25 19:11:34 +0900 provision and would not call the function unless the user has CONNECT privilege. --> -《機械翻訳》«The underlying server function permits users who have pg_read_all_stats privileges to see the sizes of all databases, even if they lack CONNECT privilege. But psql was unaware of that provision and would not call the function unless the user has CONNECT privilege.» +《機械翻訳》基礎となるサーバ関数では、pg_read_all_stats権限を持つユーザは、すべてのデータベースのサイズを参照することができます。 +これは、データベースが存在しない場合でも同様ですCONNECT権限。 +しかし、psqlはこの規定を認識しておらず、ユーザがCONNECT権限を持たない限り、関数を呼び出ししませんでした。 @@ -3920,7 +4015,7 @@ Branch: REL_14_STABLE [2d44bb900] 2026-07-03 13:50:51 +0900 for \df to consider procedures too (Erik Wienhold) --> -《機械翻訳》«Fix psql's tab completion for \df to consider procedures too » +《機械翻訳》psqlののタブ完了\df手順も検討 (Erik Wienhold) § @@ -3940,7 +4035,7 @@ Branch: REL_15_STABLE [f18fcd9a4] 2026-05-14 12:31:43 +0900 Fix thread-safety bug in pgbench (Fujii Masao) --> -《機械翻訳》«Fix thread-safety bug in pgbench » +《機械翻訳》スレッド安全バグをpgbench. (Fujii Masao) § @@ -3952,7 +4047,7 @@ Branch: REL_15_STABLE [f18fcd9a4] 2026-05-14 12:31:43 +0900 could attempt to use the same buffer to construct error messages, leading to corrupted log output. --> -《機械翻訳》«When pgbench runs with multiple threads and the option, different threads could attempt to use the same buffer to construct error messages, leading to corrupted log output.» +《機械翻訳》pgbenchがマルチプルスレッドとオプションで実行されると、異なるスレッドが同じバッファからコンストラクトへのエラーメッセージを使用しようとする可能性があり、ログの出力が破損する可能性があります。 @@ -3968,7 +4063,7 @@ Branch: REL_17_STABLE [090ce6934] 2026-06-29 13:02:07 +0200 In pg_combinebackup, prevent infinite loop if the source file is shorter than expected (Peter Eisentraut) --> -《機械翻訳》«In pg_combinebackup, prevent infinite loop if the source file is shorter than expected » +《機械翻訳》pg_combinebackupでは、無限ループが予想より短い場合はソースファイルを防止します。 (Peter Eisentraut) § @@ -3986,7 +4081,7 @@ Branch: REL_17_STABLE [c03784a21] 2026-05-27 10:35:49 +0900 Fix cleanup of publisher-side objects after errors in pg_createsubscriber (Nisha Moond) --> -《機械翻訳》«Fix cleanup of publisher-side objects after errors in pg_createsubscriber » +《機械翻訳》pg_createsubscriberでエラーが発生した後のパブリッシャー側オブジェクトのクリーンアップを修正しました。 (Nisha Moond) § @@ -3998,7 +4093,8 @@ Branch: REL_17_STABLE [c03784a21] 2026-05-27 10:35:49 +0900 publication and replication slot that it created on the publisher. Some error cases failed to do so. --> -《機械翻訳》«When pg_createsubscriber fails after creating logical replication objects, it should remove the publication and replication slot that it created on the publisher. Some error cases failed to do so.» +《機械翻訳》論理レプリケーションオブジェクトを作成した後にpg_createsubscriberが失敗した場合は、パブリッシャーに作成したパブリケーションとレプリケーションスロットを削除する必要があります。 +そうしなかったエラーのケースもあります。 @@ -4018,7 +4114,7 @@ Branch: REL_14_STABLE [5552a15a3] 2026-05-20 15:57:19 +0900 for pg_recvlogical output files (Fujii Masao) --> -《機械翻訳》«Use the source cluster's group-read file permissions for pg_recvlogical output files » +《機械翻訳》ソースクラスタのグループアクセス権pg_recvlogicalファイルを出力するためのファイル読み取り権限を使用します。 (Fujii Masao) § @@ -4028,7 +4124,7 @@ Branch: REL_14_STABLE [5552a15a3] 2026-05-20 15:57:19 +0900 pg_recvlogical was documented to behave this way, but it never actually enabled group-read. --> -《機械翻訳》«pg_recvlogical was documented to behave this way, but it never actually enabled group-read.» +《機械翻訳》pg_recvlogicalはこのように動作すると説明されていましたが、実際にグループが読めるようにはなっていませんでした。 @@ -4047,7 +4143,7 @@ Branch: REL_18_STABLE [477efef08] 2026-06-16 15:58:17 +0900 or (Chao Li, Michael Paquier) --> -《機械翻訳》«Fix inconsistent behavior of pg_restore with or » +《機械翻訳》またはを持つpg_リストアの一貫性のない動作を修正しました。 (Chao Li, Michael Paquier) § § @@ -4060,7 +4156,7 @@ Branch: REL_18_STABLE [477efef08] 2026-06-16 15:58:17 +0900 expected items, unlike pg_dump with similar options. --> -《機械翻訳》«When combined with other selective-restore options such as , these options failed to restore the expected items, unlike pg_dump with similar options.» +《機械翻訳》のような他の選択的リストアオプションと組み合わせると、これらのオプションはpg_dump類似のリストアとは異なり、期待された項目をオプションすることができなかった。 @@ -4075,7 +4171,7 @@ Branch: REL_18_STABLE [7e085aabd] 2026-06-17 09:18:39 -0500 Fix vacuumdb --missing-stats-only to ignore partitioned expression indexes (Baji Shaik) --> -《機械翻訳》«Fix vacuumdb --missing-stats-only to ignore partitioned expression indexes » +《機械翻訳》vacuumdb --missing-stats-onlyパーティション化された式インデックスを無視するように修正しました。 (Baji Shaik) § @@ -4087,7 +4183,8 @@ Branch: REL_18_STABLE [7e085aabd] 2026-06-17 09:18:39 -0500 nothing since statistics are never created for partitioned indexes, only for their leaf indexes. --> -《機械翻訳》«Previously, vacuumdb would always attempt to ANALYZE the partitioned table, accomplishing nothing since statistics are never created for partitioned indexes, only for their leaf indexes.» +《機械翻訳》以前は、vacuumdbは常にANALYZEパーティション化されたテーブルを試みていました。 +統計処理はパーティション化されたインデックスに対して作成されることはなく、リーフインデックスに対してのみ作成されるため、何も達成されませんでした。 @@ -4103,7 +4200,7 @@ Branch: REL_18_STABLE [12c32bbc8] 2026-06-12 09:39:19 +0900 corruption of a btree metapage's allequalimage flag (Chao Li) --> -《機械翻訳》«In contrib/amcheck, fix failure to report corruption of a btree metapage's allequalimage flag » +《機械翻訳》contrib/amcheckbtreeメタページallequalimageレポートのフラグの破損に対する失敗を修正しました。 (Chao Li) § @@ -4121,7 +4218,7 @@ Branch: REL_18_STABLE [1f8ab91c1] 2026-07-06 09:32:30 +0900 In contrib/amcheck, fix query-lifespan memory leak while verifying a GIN index (Kirill Reshke) --> -《機械翻訳》«In contrib/amcheck, fix query-lifespan memory leak while verifying a GIN index » +《機械翻訳》contrib/amcheckでは、問い合わせメモリリークを検証する際にGIN寿命インデックスを修正しました。 (Kirill Reshke) § @@ -4142,7 +4239,7 @@ Branch: REL_14_STABLE [af09b18cb] 2026-06-14 04:06:43 +0300 In contrib/amcheck, handle short-header varlena datums correctly (Andrey Borodin) --> -《機械翻訳》«In contrib/amcheck, handle short-header varlena datums correctly » +《機械翻訳》contrib/amcheck,ハンドルshort-ヘッダvarlenaデータムで正しく動作するようになりました。 (Andrey Borodin) § @@ -4152,7 +4249,7 @@ Branch: REL_14_STABLE [af09b18cb] 2026-06-14 04:06:43 +0300 This error could result in doing excess work while verifying a btree index, but seems not to have had any worse consequences. --> -《機械翻訳》«This error could result in doing excess work while verifying a btree index, but seems not to have had any worse consequences.» +《機械翻訳》このエラーは、btreeインデックスを検証する際に過剰な作業を行う可能性がありますが、これ以上悪い結果にはならなかったようです。 @@ -4173,7 +4270,7 @@ Branch: REL_14_STABLE [255bce448] 2026-07-01 13:27:22 -0400 fix NaN handling in the float4 and float8 opclasses (Bill Kim, Tom Lane) --> -《機械翻訳》«In contrib/btree_gist, fix NaN handling in the float4 and float8 opclasses » +《機械翻訳》contrib/btree_GiSTで、opclass NaNfloat4float8のハンドリングを修正しました。 (Bill Kim, Tom Lane) § @@ -4187,7 +4284,8 @@ Branch: REL_14_STABLE [255bce448] 2026-07-01 13:27:22 -0400 after installing this update, if there is any possibility that there are NaN entries in those columns. --> -《機械翻訳》«Comparisons, as well as the GiST penalty and distance functions, did not account for NaN and would give the wrong answer when handed one. It is recommended to reindex btree_gist indexes on float columns after installing this update, if there is any possibility that there are NaN entries in those columns.» +《機械翻訳》比較は、GiSTペナルティ関数や遠隔関数と同様に、アカウントではありませんでしたNaNそして、渡されたときに間違いの答えを返します。 +これらの列にNaNエントリがある可能性がある場合は、この更新をインストールした後にインデックス再作成btree_GiSTfloat列のインデックスを行うことをお勧めします。 @@ -4204,7 +4302,7 @@ Branch: REL_18_STABLE [558c4ea9a] 2026-07-03 13:11:14 -0400 of bit/varbit entries during GiST index construction (Tom Lane) --> -《機械翻訳》«In contrib/btree_gist, fix sorting of bit/varbit entries during GiST index construction » +《機械翻訳》contrib/btree_GiSTでは、並べ替えインデックスの構築中にビット/varbitエントリのGiSTを修正しました。 (Tom Lane) § @@ -4218,7 +4316,9 @@ Branch: REL_18_STABLE [558c4ea9a] 2026-07-03 13:11:14 -0400 reindex btree_gist indexes on bit columns after installing this update. --> -《機械翻訳》«Values of bit types were sorted as though they were byteas, which did not cause any obvious failure but would result in an inefficient index, since the types' representations are different. It is recommended to reindex btree_gist indexes on bit columns after installing this update.» +《機械翻訳》ビット型の値はbyteaであるかのようにソートされました。 +これは明らかな失敗を引き起こしませんでしたが、型の表現が異なるため、非効率的なインデックスになりました。 +このインデックス再作成をインストールした後に、ビットbtree_gist更新列のインデックスを行うことをお勧めします。 @@ -4238,7 +4338,7 @@ Branch: REL_14_STABLE [286f9a3ce] 2026-07-03 13:50:14 -0400 In contrib/btree_gist, fix searches using a not-equal operator (Ayush Tiwari) --> -《機械翻訳》«In contrib/btree_gist, fix searches using a not-equal operator » +《機械翻訳》contrib/btree_gistで、等しくない演算子を使用した検索を修正しました。 (Ayush Tiwari) § @@ -4249,7 +4349,7 @@ Branch: REL_14_STABLE [286f9a3ce] 2026-07-03 13:50:14 -0400 pages applied the wrong comparison function, leading to wrong results and potentially crashes. --> -《機械翻訳》«For variable-length data types, the code for scanning non-leaf index pages applied the wrong comparison function, leading to wrong results and potentially crashes.» +《機械翻訳》変数-長さデータタイプの場合、リーフインデックス以外のページをスキャンするコードは間違い比較関数を適用していたため、間違いの結果が得られ、クラッシュする可能性がありました。 @@ -4268,7 +4368,7 @@ Branch: REL_18_STABLE [130396e6c] 2026-05-26 00:52:38 +0900 setting for use_scram_passthrough overrides one for a foreign server (Matheus Alcantara) --> -《機械翻訳》«In contrib/dblink and contrib/postgres_fdw, ensure that a user-mapping setting for use_scram_passthrough overrides one for a foreign server » +《機械翻訳》contrib/dblinkおよびcontrib/postgres_fdw,保証では、ユーザ-マッピングの設定use_scram_passthroughは外部サーバの設定より優先されます。 (Matheus Alcantara) § § @@ -4279,7 +4379,7 @@ Branch: REL_18_STABLE [130396e6c] 2026-05-26 00:52:38 +0900 Previously the precedence went the other way, but that is inconsistent with the behavior of other foreign-table options. --> -《機械翻訳》«Previously the precedence went the other way, but that is inconsistent with the behavior of other foreign-table options.» +《機械翻訳》以前は優先順位が逆でしたが、それは他の外国-テーブルオプションの行動と矛盾しています。 @@ -4295,7 +4395,7 @@ Branch: REL_18_STABLE [cd777e27e] 2026-05-26 01:08:47 +0900 on contrib/dblink foreign-data wrappers (Matheus Alcantara) --> -《機械翻訳》«Reject setting use_scram_passthrough on contrib/dblink foreign-data wrappers » +《機械翻訳》contrib/dblink外部通過地点ラッパーの設定use_scram_データを拒否する。 (Matheus Alcantara) § @@ -4306,7 +4406,7 @@ Branch: REL_18_STABLE [cd777e27e] 2026-05-26 01:08:47 +0900 mappings, but dblink incorrectly allowed it at the FDW level as well (and then ignored it). --> -《機械翻訳》«This option is only meaningful on foreign servers and user mappings, but dblink incorrectly allowed it at the FDW level as well (and then ignored it).» +《機械翻訳》このオプションは外部サーバとユーザのマッピングでのみ意味がありますが、dblinkは誤ってFDWレベルでもこのオプションを許可しました(そして無視しました)。 @@ -4334,7 +4434,7 @@ Branch: REL_14_STABLE [1f6b2295f] 2026-06-18 12:22:55 -0400 contrib/jsonb_plpython (Aleksander Alekseev) --> -《機械翻訳》«Fix unguarded recursion and loops in contrib/hstore_plperl, contrib/jsonb_plperl, and contrib/jsonb_plpython » +《機械翻訳》contrib/hstore_plperl,contrib/jsonb_plperl,and contrib/jsonb_plpython.における無防備な再帰とループを修正しました。 (Aleksander Alekseev) § § @@ -4347,7 +4447,7 @@ Branch: REL_14_STABLE [1f6b2295f] 2026-06-18 12:22:55 -0400 loop caused when attempting to dereference circular chains of Perl object references. --> -《機械翻訳》«Prevent stack overflow when dealing with deeply nested jsonb values, and allow interruption of the infinite loop caused when attempting to dereference circular chains of Perl object references.» +《機械翻訳》深くネストされたスタックオーバーフロー値を処理するときのjsonbを防止し、Perl無限ループ参照の循環チェーンを逆参照しようとするときに発生するオブジェクトの中断を許可します。 @@ -4366,7 +4466,7 @@ Branch: REL_14_STABLE [a96b051a9] 2026-05-25 18:15:49 -0400 Fix missed release of statistics catcache entry in contrib/intarray (Man Zeng) --> -《機械翻訳》«Fix missed release of statistics catcache entry in contrib/intarray » +《機械翻訳》contrib/intarrayにある統計処理catcacheエントリの見逃しリリースを修正しました。 (Man Zeng) § @@ -4376,7 +4476,7 @@ Branch: REL_14_STABLE [a96b051a9] 2026-05-25 18:15:49 -0400 This oversight led to warnings like resource was not closed: cache pg_statistic. --> -《機械翻訳》«This oversight led to warnings like resource was not closed: cache pg_statistic.» +《機械翻訳》この見落としにより、resource is not閉じた:キャッシュpg_statisticのような警告が発生しました。 @@ -4395,7 +4495,8 @@ Branch: REL_14_STABLE [f528a5606] 2026-06-16 09:31:23 +0300 In contrib/ltree, fix integer overflow in comparisons (Ayush Tiwari) --> -《機械翻訳》«In contrib/ltree, fix integer overflow in comparisons » +《機械翻訳》では、整数オーバーフローを比較して修正します。 +contrib/ltree (Ayush Tiwari) § @@ -4407,7 +4508,8 @@ Branch: REL_14_STABLE [f528a5606] 2026-06-16 09:31:23 +0300 index contains such values, it is probably corrupt and should be reindexed after installing this update. --> -《機械翻訳》«ltree values containing more than about 14,653 labels resulted in wrong comparison answers due to overflow. If a btree index contains such values, it is probably corrupt and should be reindexed after installing this update.» +《機械翻訳》ltree値に約14,653を超えるラベルが含まれている場合、オーバーフローのために間違い比較の回答が得られました。 +btreeインデックス包含の値の場合、おそらく破損しているため、この更新をインストールした後にインデックスを再作成する必要があります。 @@ -4424,7 +4526,7 @@ Branch: REL_17_STABLE [2aa6be6e6] 2026-06-22 12:59:16 -0400 after encountering an error while using an OSSLCipher object (Yuelin Wang) --> -《機械翻訳》«In contrib/pgcrypto, avoid double-free crash after encountering an error while using an OSSLCipher object » +《機械翻訳》で、contrib/pgcrypto OSLSCipherフリーを使用中にクラッシュに遭遇した後は、二重エラーオブジェクトを使用しないでください。 (Yuelin Wang) § @@ -4442,7 +4544,7 @@ Branch: REL_18_STABLE [3bf2cb225] 2026-06-26 19:48:20 +0200 in contrib/pg_prewarm's autoprewarm worker (Matheus Alcantara) --> -《機械翻訳》«Fix out-of-bounds access in contrib/pg_prewarm's autoprewarm worker » +《機械翻訳》境界外のアクセスをcontrib/pg_prewarmのautoprewarmワーカーで修正します。 (Matheus Alcantara) § @@ -4452,7 +4554,7 @@ Branch: REL_18_STABLE [3bf2cb225] 2026-06-26 19:48:20 +0200 The code tried to fetch a value from one past the end of an array, risking a segfault. --> -《機械翻訳》«The code tried to fetch a value from one past the end of an array, risking a segfault.» +《機械翻訳》コードは、配列の端を過ぎたところから値をフェッチしようとし、セグフォールトの危険を冒しました。 @@ -4472,7 +4574,7 @@ Branch: REL_14_STABLE [1eda3eb07] 2026-06-06 08:16:46 +0900 heap_force_kill and heap_force_freeze functions (Michael Paquier) --> -《機械翻訳》«Fix array overrun in contrib/pg_surgery's heap_force_kill and heap_force_freeze functions » +《機械翻訳》contrib/pg_surgeryheap_force_killおよびheap_force_freeze関数の配列オーバーランを修正しました。 (Michael Paquier) § @@ -4483,7 +4585,7 @@ Branch: REL_14_STABLE [1eda3eb07] 2026-06-06 08:16:46 +0900 MaxHeapTuplesPerPage wrote one byte past the end of the allocated array, potentially crashing the server. --> -《機械翻訳》«Attempting to change a TID whose offset number equals MaxHeapTuplesPerPage wrote one byte past the end of the allocated array, potentially crashing the server.» +《機械翻訳》TID番号がMaxHeapTuplePerPageに等しいオフセットを変更しようとすると、割り当てられたバイトの末尾を超えて1つの配列が書き込まれ、サーバがクラッシュする可能性がありました。 @@ -4503,7 +4605,7 @@ Branch: REL_14_STABLE [e9d53cf45] 2026-08-04 11:44:11 +0200 In contrib/pg_surgery, avoid infinite loop with TID arrays having more than 64K elements (Andrey Rachitskiy) --> -《機械翻訳》«In contrib/pg_surgery, avoid infinite loop with TID arrays having more than 64K elements » +《機械翻訳》では、contrib/pg_surgery 64K以上の要素を持つ無限ループ配列を持つTIDは避けてください。 (Andrey Rachitskiy) § @@ -4525,7 +4627,7 @@ Branch: REL_14_STABLE [1de0a711d] 2026-05-14 13:11:49 -0500 in contrib/refint's check_foreign_key() (Ayush Tiwari) --> -《機械翻訳》«Avoid NULL-pointer dereference in contrib/refint's check_foreign_key() » +《機械翻訳》contrib/refintcheck_foreign_key().*でのNULL-ポインタの逆参照を避ける。 (Ayush Tiwari) § @@ -4536,7 +4638,8 @@ Branch: REL_14_STABLE [1de0a711d] 2026-05-14 13:11:49 -0500 led to a crash. This is an oversight in the fix for CVE-2026-6637, but the code that was there before that wasn't really right either. --> -《機械翻訳》«In the on-update-cascade case, a null value of a referenced column led to a crash. This is an oversight in the fix for CVE-2026-6637, but the code that was there before that wasn't really right either.» +《機械翻訳》更新カスケードケースでは、被参照カラムのNULL値がクラッシュにつながりました。 +これはCVE-2026-6637の修正における見落としですが、以前に存在していたコードも実際には正しくありませんでした。 @@ -4556,7 +4659,7 @@ Branch: REL_14_STABLE [58b91fc73] 2026-06-11 12:34:45 +0300 with ~ certainty indicators correctly (Ewan Young) --> -《機械翻訳》«Fix contrib/seg to print segments with ~ certainty indicators correctly » +《機械翻訳》contrib/segをプリントセグメントに~確実度インジケータで正しく修正した。 (Ewan Young) § @@ -4570,7 +4673,8 @@ Branch: REL_14_STABLE [58b91fc73] 2026-06-11 12:34:45 +0300 the upper boundary was not printed at all, incorrectly converting the value into an open interval. --> -《機械翻訳》«Due to a typo, seg_out() did not print a ~ certainty indicator attached to a segment's upper boundary. Worse, if the lower boundary had ~ while the upper boundary had no indicator, the upper boundary was not printed at all, incorrectly converting the value into an open interval.» +《機械翻訳》タイポのために、seg_out()~セグメントの上部境界にアタッチされた確実性指示子を印刷しませんでした。 +さらに悪いことに、下部境界に~があり、上部境界には指示子がない場合、上部境界はまったく印刷されず、値がオープンインターバルに誤って変換されました。 @@ -4590,7 +4694,7 @@ Branch: REL_14_STABLE [f3f901a53] 2026-06-11 14:29:29 +0900 xpath_nodeset() function (Andrey Chernyy, Michael Paquier) --> -《機械翻訳》«Fix crash with namespace nodes in contrib/xml2's xpath_nodeset() function » +《機械翻訳》contrib/xml2xpath_nodeset()関数の名前空間ノードでクラッシュを修正しました。 (Andrey Chernyy, Michael Paquier) § @@ -4611,7 +4715,7 @@ Branch: REL_14_STABLE [086652c02] 2026-06-12 13:57:22 +0200 Support building PostgreSQL with OpenSSL 4 (Daniel Gustafsson) --> -《機械翻訳》«Support building PostgreSQL with OpenSSL 4 » +《機械翻訳》サポートの建物PostgreSQLOpenSSL 4. (Daniel Gustafsson) § @@ -4633,7 +4737,7 @@ Branch: REL_14_STABLE [812cc1a73] 2026-08-02 11:26:30 -0400 Update time zone data files to tzdata release 2026c (Tom Lane) --> -《機械翻訳》«Update time zone data files to tzdata release 2026c » +タイムゾーンデータファイルがtzdataリリース2026cに更新されました。 (Tom Lane) § @@ -4646,7 +4750,9 @@ Branch: REL_14_STABLE [812cc1a73] 2026-08-02 11:26:30 -0400 be CST from that time forward. That seems likely to change, but it's unclear what new abbreviation will be used. --> -《機械翻訳》«Alberta (America/Edmonton) will be on year-round UTC-06 (effectively, permanent DST) beginning in November 2026. This release assumes that their TZ abbreviation will be CST from that time forward. That seems likely to change, but it's unclear what new abbreviation will be used.» +《機械翻訳》アルバータ(アメリカ/エドモントン)は、2026年11月から1年を通してUTC-06(事実上の恒久的なDST)になります。 +このリリースでは、TZの省略形がCSTそれ以降になると想定しています。 +これは変更される可能性がありますが、新しい省略形が使用されるかどうかは不明です。 @@ -4654,7 +4760,7 @@ Branch: REL_14_STABLE [812cc1a73] 2026-08-02 11:26:30 -0400 Morocco (Africa/Casablanca) will move to permanent UTC+00, without daylight saving time transitions, on 2026-09-20. --> -《機械翻訳》«Morocco (Africa/Casablanca) will move to permanent UTC+00, without daylight saving time transitions, on 2026-09-20.» +《機械翻訳》モロッコ(Africa/Casablanca)は、2026-09-20に夏時間の移行なしで恒久的なUTC+00に移行します。 @@ -5158,7 +5264,8 @@ Branch: REL_14_STABLE [b282280e9] 2026-05-11 05:13:51 -0700 --> 《マッチ度[89.389068]》パスワードやハッシュなどの検証には、 memcpy()strcmp()の代わりにtimingsafe_bcmp()を使用するようになりました。 これらの関数のデータ依存性が、これらの箇所で悪用される可能性があるかどうかは不明ですが、安全を期してこれらが置き換えられました。 -《機械翻訳》«Use timingsafe_bcmp() instead of memcmp() or strcmp() when checking passwords, hashes, etc. It is not known whether the data dependency of those functions is usefully exploitable in any of these places, but in the interests of safety, replace them.» +《機械翻訳》使用timingsafe_bcmp()代わりmemcmp()または、パスワード、ハッシュなどをチェックする場合には、または、これらの関数のデータ依存性がこれらの場所のいずれかで有効に利用できるかどうかは不明であるが、安全のためにそれらを置き換える。 +strcmp()