Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wdYbw-004BRg-2P for pgsql-bugs@arkaria.postgresql.org; Sat, 27 Jun 2026 19:22:41 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wdYbu-00Fhrj-0A for pgsql-bugs@arkaria.postgresql.org; Sat, 27 Jun 2026 19:22:38 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wdYbt-00FhrZ-2M for pgsql-bugs@lists.postgresql.org; Sat, 27 Jun 2026 19:22:37 +0000 Received: from mail-oa1-x32.google.com ([2001:4860:4864:20::32]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wdYbl-00000000X63-3W2R for pgsql-bugs@lists.postgresql.org; Sat, 27 Jun 2026 19:22:31 +0000 Received: by mail-oa1-x32.google.com with SMTP id 586e51a60fabf-43d3a0dabb0so1272676fac.1 for ; Sat, 27 Jun 2026 12:22:28 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1782588146; cv=none; d=google.com; s=arc-20260327; b=Zm8HFnfv6Q0+y79Ze80RG1LjY9QD9o3sXsLcMaXkQnb1zN9ud6pITlRpjlFY65VGvy xLDM19hvVB0mwb9LixjJf20Jf9NqpEoriA8fwrElcyFT1ek6sX9q8acxrQ/duU70IK+E MsTVdc84SYluuzo0ogXugt+BCGqZFfmHlDDoWgjT8asaH/240sl1pAco763fqK6BfQAB 3SbtajG6mYb2ScsWHuHdwI7rBECFisnigMDn49SB6+yUMIoEb1fbpVob97eCX69aX7EP 06+imQa39k2nKZ9S0lR7ChRsH/pP0JxBn3aD5kh7zaJtU4Av1MxkuKox7qi3nWb9xEzp 2Bjg== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20260327; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:dkim-signature; bh=hA5w6UTcBq8LfOkvbvZcQAwqQKMQJ+uIItOmXNQi3fo=; fh=qK8pXRLhBG8xtHlkPLpw6BHy4wT2siIvOnetoZQT5T8=; b=g4oaAXavD1Q2Axe3+5QaKHdB/6UIGpruqjmc/VcD1K1Ai4edEp5U53M4JO9Dafz8Hi LMvin7WGBSahWnEwqxNg68qAYuDT/UFmhIJGpp2yK8wTGOp71tY/ls3pUr+B+45sLjBN FzoA4hpwsJyVCHdT4/WiZSamLVt5XqKoSWK15T9GqkvbrZ/y9EcS2HR2SGqvV2z0bxpQ CzwLQwIz3Wibfb6VLeFeUFEKClOcDQPbNh4OxynSHdjuPHngBGencTn0axYzI5LQkNS6 EcR4pCRDhg/ar+QQpykddnc/dzTR+HNRSAXsoxFOJa6KFdzUs9mnpKpacVRA2wT6IBI8 vedw==; darn=lists.postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782588146; x=1783192946; darn=lists.postgresql.org; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=hA5w6UTcBq8LfOkvbvZcQAwqQKMQJ+uIItOmXNQi3fo=; b=fbaa98aVTbGcKzPzb5nvDvLIZLBhW3nnbGBOejN0tjbDi7HgM7ILLkX3wxcwFgK9fT 2vIqS5KJc4gJ/Nvu3ESKsTKU78I5MH8K3qXrvhMPEsymjU2KpBTKFIEw/2o5g6usIxDX QrxPENdBTukxOhqdd/UYiwAOuRMZ4hG2rJ2YOgUQzCOW+jUh0dCR1lOuz5+dFkomRpHC Wq5/rycUOVHZi6MXF/DRCxViSd+qwJrITkPWI5tqS6Tp2Z73rhDsTZ9Rt6I9BgukobqX kolM5N8bv+GOK9YWo2Rbm905Bv/t24qXJtLimGAZjNIjov+KD+lUA2HGivCbNGQKv87M Euig== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782588146; x=1783192946; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=hA5w6UTcBq8LfOkvbvZcQAwqQKMQJ+uIItOmXNQi3fo=; b=TyTKNMHJ5GJR3TK2GERYhOHySqmij2wMH7jf44fuBtS6yrACT9DkYDfey+KNkPurR1 wDqcOT8IFQnAKW0Bvyocw2L6eXPYpruAs70oQqlmyKFyRJ6nWkGrfzvI1g1AGt4ICuP7 0LN9kcLffXxWV+0td1ddSLNFaJ3PDLUJjHZ1DbZ+CiVh4kH8XB3JLXVm1WbMy/90egd3 Jidk/Bu0YGu2rqpsarSt1f5/SsDbyBntRb9QcyBIu3aJ9XUyUsGcPFqHWSO6JoNbRl3I 6TOAdL9lT/b+feTOmtRYZZjbHsG6a+CqDI5j7rRgNyjhBtaIU8otj2ItnwHKXuRxLTBL 342g== X-Forwarded-Encrypted: i=1; AFNElJ8Kp0M8RuWrrxHraoh7wccWkdHNSgZPrhn0s79DYzxTXZFIgXJKU+/4jr1zNFG4kRWfFz7X14uLrMdv@lists.postgresql.org X-Gm-Message-State: AOJu0YzwhuD14mYbrmU6QRJh4QC+waNXCcqEJpqynj8rVcolfQ1rU7b3 HSeETOheoqHRZukKjvfNo6Skv6NW+MdHCTkDjJL31qrkOs7uQ8hdGxScQpBXT6mbhwRzllbwQJQ +K6mJu+UI4X480YEMY47ON7lFmQjg6xo= X-Gm-Gg: AfdE7cknOe7icvX/mZv4+dxoSvt6QkIiOmHcmk0uJW18lcmjhR/6vrKDD6BqTuYPK81 9fnAt43X0JdtBleAzkdJnY03TBvdf67HZoy4BC15AwOoWL0WBGzZFMy6QLmi0OpLMsiiiEvhzmZ Nbzgq2la7rk7zMnbWw/FXZFEf2FAYjXshcxg5ksKmsNQyVA9H77qEI6hKf5GviLIMbmZxiC8+3u RorL8Vd6OjARTEil/jHD62790xkI4imyxmn8IGKLR8RjqQiTn1sO5ZsOz8F8e0IR0ToKDjw2c2q 1ZZvIzv9/zbTrK2h/wanQsSiRyr2 X-Received: by 2002:a05:6808:11c7:b0:48a:a6d8:890e with SMTP id 5614622812f47-49218513336mr8837043b6e.41.1782588146358; Sat, 27 Jun 2026 12:22:26 -0700 (PDT) MIME-Version: 1.0 References: <19536-73ce5847e6c0e7b1@postgresql.org> In-Reply-To: <19536-73ce5847e6c0e7b1@postgresql.org> From: Bharath Rupireddy Date: Sat, 27 Jun 2026 12:22:14 -0700 X-Gm-Features: AVVi8Cf1BUuMw74vztd0n1RXvQBlaknS-2RowWBeUWVvFrHESoJzKnXaxh-d72I Message-ID: Subject: Re: BUG #19536: UPDATE RETURNING OLD value is stale after concurrent update when table has a BEFORE UPDATE trigger To: bobergj@gmail.com, pgsql-bugs@lists.postgresql.org Cc: Dean Rasheed Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk Hi, On Sat, Jun 27, 2026 at 5:54=E2=80=AFAM PG Bug reporting form wrote: > > The following bug has been logged on the website: > > Bug reference: 19536 > Logged by: Jonas Boberg > Email address: bobergj@gmail.com > PostgreSQL version: 18.4 > Operating system: Docker image postgres:18.4-alpine3.23 > Description: Thanks for reporting the bug and a reproducer! > In an UPDATE ... RETURNING OLD under isolation level READ COMMITTED, when > the table has a BEFORE UPDATE trigger, the OLD value is stale after a > concurrent update. > > SELECT version(); > > PostgreSQL 18.4 on aarch64-unknown-linux-musl, compiled by gcc (Alpine > 15.2.0) 15.2.0, 64-bit > > ## How to reproduce > > Setup: > ------- > CREATE TABLE t ( > id int PRIMARY KEY, > n int NOT NULL > ); > > CREATE FUNCTION before_update_noop() > RETURNS trigger > LANGUAGE plpgsql AS $$ > BEGIN > RETURN NEW; > END; > $$; > > CREATE TRIGGER t_before_update > BEFORE UPDATE ON t > FOR EACH ROW EXECUTE FUNCTION before_update_noop(); > > INSERT INTO t VALUES (1, 7); > ----- > > psql session 1: > ------ > BEGIN ISOLATION LEVEL READ COMMITTED; > > UPDATE t > SET n =3D n + 10 > WHERE id =3D 1; > -- do not commit, hold the row lock > ------ > Leave session 1 open > > psql session 2: > ----- > BEGIN ISOLATION LEVEL READ COMMITTED; > UPDATE t > SET n =3D n + 1 > WHERE id =3D 1 > RETURNING > OLD.n AS old_n, > NEW.n AS new_n, > NEW.n - 1 AS expected_old; > ---- > Session 2 blocks. Now commit in session 1: > ---- > COMMIT; > ---- > > ## Actual result > > Session 2 outputs: > old_n | new_n | expected_old > -------+-------+-------------- > 7 | 18 | 17 > (1 row) > > Here OLD.n is the pre-concurrent update value, while NEW.n is the value > written by the concurrent update. > > ## Expected result > > old_n | new_n | expected_old > -------+-------+-------------- > 17 | 18 | 17 > > If I drop the trigger and repeat the test, session 2 returns the expected > result. I reproduced it on HEAD. Backend 1 stores its txn-id in the old row's xmax (ctid=3D(0,1), n=3D7) and is still in progress. Backend 2 tries to lock the same row, sees that xmax holds an in-progress txn, and waits (ExecUpdate -> ExecUpdatePrologue -> ExecBRUpdateTriggers -> GetTupleForTrigger -> table_tuple_lock -> heap_lock_tuple -> XactLockTableWait). When backend 1 commits, backend 2 wakes up. Because the lock was requested with TUPLE_LOCK_FLAG_FIND_LAST_VERSION (set under READ COMMITTED in GetTupleForTrigger), heap_lock_tuple walks the row-version chain to the latest version and advances tupleid from (ctid=3D(0,1), n=3D7) to (ctid=3D(0,2), n=3D17). In ExecUpdate after ExecUpdatePrologue, tupleid now points at the latest version (ctid=3D(0,2), n=3D17) and the lock is already held, so table_tuple_update has nothing to wait on and returns TM_Ok. But the oldSlot - the slot RETURNING clause reads from - is only refreshed on the TM_Updated path. Since we got TM_Ok, oldSlot still holds the value captured before waiting (ctid=3D(0,1), n=3D7). NEW is correct (ctid=3D(0,3)= , n=3D18). In short: with a BEFORE UPDATE trigger, the trigger's tuple lock advances tupleid to the concurrently-updated row version, so the later table_tuple_update returns TM_Ok instead of TM_Updated. Since oldSlot is only refreshed on the TM_Updated path, the RETURNING clause keeps the pre-wait value (ctid=3D(0,1), n=3D7) instead of the actual concurrently-updated value (ctid=3D(0,2), n=3D17). Without the trigger, the wait happens inside table_tuple_update itself, which returns TM_Updated and correctly refreshes oldSlot. I'm adding Dean Rasheed (commit 80feb727c86 v18 - Add OLD/NEW support to RETURNING in DML queries.) for additional inputs. -- Bharath Rupireddy Amazon Web Services: https://aws.amazon.com