From 7494b73918590be0f8c16457594565985094573f Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Wed, 14 Oct 2026 01:43:21 +0000 Subject: [PATCH 1/2] test: the row-identity lock and its two settings, in both twins (#1341) Two #1341 High items: pgcolumnar.enable_row_update_lock and pgcolumnar.row_lock_buckets were set or read by no test. New arms in advisory_lock_class, both twins: - an UPDATE holds a class-104 row-identity lock while the setting is on; - it holds none with the setting off; - row_lock_buckets refuses SET with 55P02 and reports context postmaster, because the bucket count is part of the lock tag. Red first: with the code ignoring the setting (the lock taken even when off), both twins report "and holds none with enable_row_update_lock off: got 1 want 0". Clean on PG 17: 10/10 and 4 pytest tests. cluster_tests 533 -> 534 by collection. advisory_lock_class has no ledger rows. Co-Authored-By: Claude Opus 5.5 --- test/advisory_lock_class.sh | 25 +++++++++++++++ test/pytest/TESTS.md | 1 + test/pytest/expected_tests.txt | 5 ++- test/pytest/test_advisory_lock_class.py | 41 +++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/test/advisory_lock_class.sh b/test/advisory_lock_class.sh index b619c96f5..7c2c6af73 100755 --- a/test/advisory_lock_class.sh +++ b/test/advisory_lock_class.sh @@ -204,4 +204,29 @@ check "a duplicate key is still rejected" \ "$(grep -qiE 'duplicate key|unique constraint' <<<"$dup" && echo rejected || echo "NOT rejected: $(head -1 <<<"$dup")")" \ "rejected" +# ---- the row-identity lock (class 104) and its two settings (#1341) ---------------- +# +# An UPDATE or DELETE serialises on the row it changes through an advisory lock in +# class 104 (PGCOLUMNAR_LOCKCLASS_ROW_IDENTITY). pgcolumnar.enable_row_update_lock +# turns that off for a session; pgcolumnar.row_lock_buckets is part of the lock tag, +# so it is fixed at server start. Neither was set or read by any test. +psql_run "CREATE TABLE rl (id int, v int) USING pgcolumnar;" +psql_run "INSERT INTO rl SELECT g, g FROM generate_series(1, 100) g;" +# row_locks_while_updating SETTING -> class-104 locks another session holds mid-UPDATE +row_locks_while_updating() { + env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -Atq \ + -c "BEGIN; SET LOCAL pgcolumnar.enable_row_update_lock = $1; UPDATE rl SET v = v + 1 WHERE id = 7; SELECT pg_sleep(3); COMMIT;" \ + >/dev/null 2>&1 & + local holder=$! + sleep 1 + q "SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND objsubid = 104 AND pid <> pg_backend_pid()" + wait "$holder" +} +check "an UPDATE holds a row-identity lock (class 104) while enable_row_update_lock is on" \ + "$([ "$(row_locks_while_updating on)" -ge 1 ] && echo held || echo none)" "held" +check "and holds none with enable_row_update_lock off" "$(row_locks_while_updating off)" "0" +check "row_lock_buckets is fixed at server start, so a session cannot change it (55P02)" \ + "$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -At -v VERBOSITY=sqlstate -c "SET pgcolumnar.row_lock_buckets = 7;" 2>&1 | sed -n 's/^.*ERROR:[[:space:]]*\([0-9A-Z]\{5\}\).*$/\1/p')|$(q "SELECT context || '|' || setting FROM pg_settings WHERE name = 'pgcolumnar.row_lock_buckets'")" \ + "55P02|postmaster|1024" + pgc_summary diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index 54025d17e..1008be473 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -5379,6 +5379,7 @@ regardless. | `test_the_lock_an_insert_takes_is_not_in_a_sql_reachable_class` | the lock is enabled, the insert's locks are visible, none is SQL-reachable, and the discovering session leaves nothing held | | `test_a_user_advisory_lock_on_that_tag_does_not_block_an_insert` | a user session holding every addressable tag in class 2 does not block a columnar insert | | `test_a_duplicate_key_is_still_rejected` | the lock still does its job, so removing the collision by removing the lock does not pass | +| `test_the_row_identity_lock_and_its_settings` | an UPDATE holds a class-104 row-identity lock with `enable_row_update_lock` on and none with it off; `row_lock_buckets` refuses `SET` (55P02) because it is fixed at server start (#1341) | ## 69. test_index_am_support.py: the index methods the page claims must work `docs/features.md` said "`CREATE INDEX` builds btree and hash indexes over a diff --git a/test/pytest/expected_tests.txt b/test/pytest/expected_tests.txt index 88fe8cb8d..f5fe02222 100644 --- a/test/pytest/expected_tests.txt +++ b/test/pytest/expected_tests.txt @@ -633,4 +633,7 @@ guard_tests 429 # test_ttl_expire.py, by collection over the cluster files on this tree. # cluster_tests 534 -> 538 for #1341's test_import_export_privilege.py, the twin of # import_export_privilege.sh: four tests, by collection over the cluster files on this tree. -cluster_tests 538 +# +# cluster_tests 538 -> 539 for #1341's test_the_row_identity_lock_and_its_settings in +# test_advisory_lock_class.py, by collection over the cluster files on this tree. +cluster_tests 539 diff --git a/test/pytest/test_advisory_lock_class.py b/test/pytest/test_advisory_lock_class.py index 3ed5b2f14..ddbe11dd0 100644 --- a/test/pytest/test_advisory_lock_class.py +++ b/test/pytest/test_advisory_lock_class.py @@ -239,3 +239,44 @@ def test_a_duplicate_key_is_still_rejected(pgc_conn, expect): got = ("rejected" if "duplicate key" in msg or "unique constraint" in msg else f"NOT rejected: {str(exc).splitlines()[0]}") expect.text(got, "rejected", "a duplicate key is still rejected") + + +def test_the_row_identity_lock_and_its_settings(pgc_cluster, pgc_conn, expect): + """An UPDATE serialises on its row through an advisory lock in class 104; the + session setting enable_row_update_lock turns that off, and row_lock_buckets is + part of the lock tag, so it is fixed at server start (#1341).""" + with pgc_conn.cursor() as cur: + cur.execute("CREATE TABLE rl (id int, v int) USING pgcolumnar") + cur.execute("INSERT INTO rl SELECT g, g FROM generate_series(1, 100) g") + cur.execute("SELECT current_schema()") + schema = cur.fetchone()[0] + + def row_locks_while_updating(setting): + holder = psycopg.connect(pgc_cluster.dsn(), autocommit=False) + try: + with holder.cursor() as hc: + hc.execute(f'SET search_path TO "{schema}"') + hc.execute(f"SET LOCAL pgcolumnar.enable_row_update_lock = {setting}") + hc.execute("UPDATE rl SET v = v + 1 WHERE id = 7") + with pgc_conn.cursor() as cur: + cur.execute("SELECT count(*) FROM pg_locks WHERE locktype = 'advisory'" + " AND objsubid = 104 AND pid <> pg_backend_pid()") + return cur.fetchone()[0] + finally: + holder.rollback() + holder.close() + + on = row_locks_while_updating("on") + expect.text("held" if on >= 1 else "none", "held", + "an UPDATE holds a row-identity lock (class 104) while enable_row_update_lock is on") + expect.num(row_locks_while_updating("off"), 0, "and holds none with enable_row_update_lock off") + with pgc_conn.cursor() as cur: + try: + cur.execute("SET pgcolumnar.row_lock_buckets = 7") + refused = "ok" + except psycopg.Error as e: + refused = e.sqlstate + cur.execute("SELECT context || '|' || setting FROM pg_settings WHERE name = 'pgcolumnar.row_lock_buckets'") + ctx = cur.fetchone()[0] + expect.text(f"{refused}|{ctx}", "55P02|postmaster|1024", + "row_lock_buckets is fixed at server start, so a session cannot change it (55P02)") From 29b651f6c999fac4d2c0fb387312e7806ff39db0 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Wed, 14 Oct 2026 02:41:15 +0000 Subject: [PATCH 2/2] test: the shell row-lock arm waits for the holder's UPDATE before it reads (#1368 review) OffgridwithJD's review: the shell twin read pg_locks after a fixed sleep, so a holder that had not reached its UPDATE yet made "and holds none with enable_row_update_lock off" read 0 on a build that ignores the setting -- nothing was locked yet. Shown with the holder delayed by pg_sleep(2) and the setting ignored, PG 17: old arm: "holds none ... off" PASSED (silently); only the on arm reddened, for the wrong reason ("got [none]") new arm: "and holds none with enable_row_update_lock off: got [1|1] want [0|1]" The arm now polls (10 s deadline) until the holder holds RowExclusiveLock on the table, and reads both counts in one query, so the second field proves the UPDATE was running when the first was read. Clean on PG 17: 10/10. Co-Authored-By: Claude Opus 5.5 --- test/advisory_lock_class.sh | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/test/advisory_lock_class.sh b/test/advisory_lock_class.sh index 7c2c6af73..9ff5f49de 100755 --- a/test/advisory_lock_class.sh +++ b/test/advisory_lock_class.sh @@ -212,19 +212,30 @@ check "a duplicate key is still rejected" \ # so it is fixed at server start. Neither was set or read by any test. psql_run "CREATE TABLE rl (id int, v int) USING pgcolumnar;" psql_run "INSERT INTO rl SELECT g, g FROM generate_series(1, 100) g;" -# row_locks_while_updating SETTING -> class-104 locks another session holds mid-UPDATE +# row_locks_while_updating SETTING -> "|", read +# in one query once the holder's UPDATE is running. WAITED FOR, NOT SLEPT ON (#1368 +# review): read after a fixed sleep, a holder that had not reached its UPDATE yet made +# the off arm read 0 on a build that ignores the setting, because nothing was locked. +# The second field proves the UPDATE was running when the first was read. row_locks_while_updating() { env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -Atq \ -c "BEGIN; SET LOCAL pgcolumnar.enable_row_update_lock = $1; UPDATE rl SET v = v + 1 WHERE id = 7; SELECT pg_sleep(3); COMMIT;" \ >/dev/null 2>&1 & - local holder=$! - sleep 1 - q "SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND objsubid = 104 AND pid <> pg_backend_pid()" + local holder=$! got="" i + for i in $(seq 1 100); do + got="$(q "SELECT count(*) FILTER (WHERE locktype = 'advisory' AND objsubid = 104) || '|' || + count(*) FILTER (WHERE locktype = 'relation' AND relation = 'rl'::regclass AND mode = 'RowExclusiveLock') + FROM pg_locks WHERE pid <> pg_backend_pid()")" + [ "${got#*|}" = 1 ] && break + sleep 0.1 + done + echo "$got" wait "$holder" } +_rl_on="$(row_locks_while_updating on)" check "an UPDATE holds a row-identity lock (class 104) while enable_row_update_lock is on" \ - "$([ "$(row_locks_while_updating on)" -ge 1 ] && echo held || echo none)" "held" -check "and holds none with enable_row_update_lock off" "$(row_locks_while_updating off)" "0" + "$([ "${_rl_on%|*}" -ge 1 ] 2>/dev/null && echo "held|${_rl_on#*|}" || echo "none|${_rl_on#*|}")" "held|1" +check "and holds none with enable_row_update_lock off" "$(row_locks_while_updating off)" "0|1" check "row_lock_buckets is fixed at server start, so a session cannot change it (55P02)" \ "$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -At -v VERBOSITY=sqlstate -c "SET pgcolumnar.row_lock_buckets = 7;" 2>&1 | sed -n 's/^.*ERROR:[[:space:]]*\([0-9A-Z]\{5\}\).*$/\1/p')|$(q "SELECT context || '|' || setting FROM pg_settings WHERE name = 'pgcolumnar.row_lock_buckets'")" \ "55P02|postmaster|1024"