From 03cf8a33df9ad2c473affd02425a65fabaa22809 Mon Sep 17 00:00:00 2001 From: Rui Zhao Date: Wed, 21 Oct 2026 00:33:09 +0800 Subject: [PATCH] Use a strong lock for unregistered relation options The first ALTER TABLE in a new backend can choose its lock before the AM library registers its options. AlterTableGetRelOptionsLockLevel() ignores unknown names, so the command takes ShareUpdateExclusiveLock even when the option requires AccessExclusiveLock. Loading the library first changes the lock taken by the same command. Return AccessExclusiveLock if any option is not registered yet. Known options retain their declared lock levels. Test SET and RESET in fresh backends, both with and without explicitly loading the table AM library. --- src/backend/access/common/reloptions.c | 6 ++ src/test/modules/dummy_table_am/meson.build | 1 + .../dummy_table_am/t/002_reloptions_lock.pl | 64 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 src/test/modules/dummy_table_am/t/002_reloptions_lock.pl diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index f43c534cb26..24dfca82350 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -2570,6 +2570,7 @@ AlterTableGetRelOptionsLockLevel(List *defList) foreach(cell, defList) { DefElem *def = (DefElem *) lfirst(cell); + bool found = false; int i; for (i = 0; relOpts[i]; i++) @@ -2578,10 +2579,15 @@ AlterTableGetRelOptionsLockLevel(List *defList) def->defname, relOpts[i]->namelen + 1) == 0) { + found = true; if (lockmode < relOpts[i]->lockmode) lockmode = relOpts[i]->lockmode; } } + + /* The AM's library may not have registered this option yet. */ + if (!found) + return AccessExclusiveLock; } return lockmode; diff --git a/src/test/modules/dummy_table_am/meson.build b/src/test/modules/dummy_table_am/meson.build index c15e54a6c62..cac2e8b2fe8 100644 --- a/src/test/modules/dummy_table_am/meson.build +++ b/src/test/modules/dummy_table_am/meson.build @@ -34,6 +34,7 @@ tests += { 'tap': { 'tests': [ 't/001_pg_dump.pl', + 't/002_reloptions_lock.pl', ], }, } diff --git a/src/test/modules/dummy_table_am/t/002_reloptions_lock.pl b/src/test/modules/dummy_table_am/t/002_reloptions_lock.pl new file mode 100644 index 00000000000..95a372a69b9 --- /dev/null +++ b/src/test/modules/dummy_table_am/t/002_reloptions_lock.pl @@ -0,0 +1,64 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Check option locking before and after loading the table AM's library. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +$node->safe_psql( + 'postgres', q{ + CREATE EXTENSION dummy_table_am; + CREATE TABLE t_std (a int) USING dummy_table_am WITH (option_int = 7); + CREATE TABLE t_custom (a int) USING dummy_custom_table_am + WITH (option_a = 5); +}); + +for my $case ([ 't_std', 'option_int' ], [ 't_custom', 'option_a' ]) +{ + my ($table, $option) = @$case; + + for my $loaded (0, 1) + { + my $load = $loaded ? "LOAD 'dummy_table_am';" : ''; + + for my $action ("SET ($option = 9)", "RESET ($option)") + { + # Each safe_psql call uses a new backend. + is( $node->safe_psql( + 'postgres', qq{ + $load + BEGIN; + ALTER TABLE $table $action; + SELECT mode FROM pg_locks + WHERE pid = pg_backend_pid() + AND relation = '$table'::regclass; + ROLLBACK; + }), + 'AccessExclusiveLock', + "$table $action with library loaded=$loaded"); + } + } +} + +# Known standard options retain their declared lock level. +is( $node->safe_psql( + 'postgres', q{ + BEGIN; + ALTER TABLE t_std SET (fillfactor = 70); + SELECT mode FROM pg_locks + WHERE pid = pg_backend_pid() AND relation = 't_std'::regclass; + ROLLBACK; +}), + 'ShareUpdateExclusiveLock', + 'standard option keeps its lock level'); + +$node->stop; +done_testing(); -- 2.43.7