From d8d5a00b182009f6fdb426a40ac677c4fda56be7 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 22 Mar 2016 17:56:06 -0400 Subject: [PATCH] Fix EvalPlanQual bug when query contains both locked and not-locked rels. In commit afb9249d06f47d7a, we (probably I) made ExecLockRows assign null test tuples to all relations of the query while setting up to do an EvalPlanQual recheck for a newly-updated locked row. This was sheerest brain fade: we should only set test tuples for relations that are lockable by the LockRows node, and in particular empty test tuples are only sensible for inheritance child relations that weren't the source of the current tuple from their inheritance tree. Setting a null test tuple for an unrelated table causes it to return NULLs when it should not, as exhibited in bug #14034 from Bronislav Houdek. To add insult to injury, doing it the wrong way required two loops where one would suffice; so the corrected code is even a bit shorter and faster. Add a regression test case based on his example, and back-patch to 9.5 where the bug was introduced. --- src/backend/executor/nodeLockRows.c | 40 +++++++++---------- .../isolation/expected/eval-plan-qual.out | 19 +++++++++ src/test/isolation/specs/eval-plan-qual.spec | 23 +++++++++++ 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/backend/executor/nodeLockRows.c b/src/backend/executor/nodeLockRows.c index b9b0f06882..d5466b29e6 100644 --- a/src/backend/executor/nodeLockRows.c +++ b/src/backend/executor/nodeLockRows.c @@ -262,29 +262,15 @@ lnext: */ if (epq_needed) { - int i; - /* Initialize EPQ machinery */ EvalPlanQualBegin(&node->lr_epqstate, estate); /* - * Transfer already-fetched tuples into the EPQ state, and make sure - * its test tuples for other tables are reset to NULL. - */ - for (i = 0; i < node->lr_ntables; i++) - { - EvalPlanQualSetTuple(&node->lr_epqstate, - i + 1, - node->lr_curtuples[i]); - /* freeing this tuple is now the responsibility of EPQ */ - node->lr_curtuples[i] = NULL; - } - - /* - * Next, fetch a copy of any rows that were successfully locked - * without any update having occurred. (We do this in a separate pass - * so as to avoid overhead in the common case where there are no - * concurrent updates.) + * Transfer any already-fetched tuples into the EPQ state, and fetch a + * copy of any rows that were successfully locked without any update + * having occurred. (We do this in a separate pass so as to avoid + * overhead in the common case where there are no concurrent updates.) + * Make sure any inactive child rels have NULL test tuples in EPQ. */ foreach(lc, node->lr_arowMarks) { @@ -293,15 +279,25 @@ lnext: HeapTupleData tuple; Buffer buffer; - /* ignore non-active child tables */ + /* skip non-active child tables, but clear their test tuples */ if (!erm->ermActive) { Assert(erm->rti != erm->prti); /* check it's child table */ + EvalPlanQualSetTuple(&node->lr_epqstate, erm->rti, NULL); continue; } - if (EvalPlanQualGetTuple(&node->lr_epqstate, erm->rti) != NULL) - continue; /* it was updated and fetched above */ + /* was tuple updated and fetched above? */ + if (node->lr_curtuples[erm->rti - 1] != NULL) + { + /* yes, so set it as the EPQ test tuple for this rel */ + EvalPlanQualSetTuple(&node->lr_epqstate, + erm->rti, + node->lr_curtuples[erm->rti - 1]); + /* freeing this tuple is now the responsibility of EPQ */ + node->lr_curtuples[erm->rti - 1] = NULL; + continue; + } /* foreign tables should have been fetched above */ Assert(erm->relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE); diff --git a/src/test/isolation/expected/eval-plan-qual.out b/src/test/isolation/expected/eval-plan-qual.out index 457461e355..5898d94ff1 100644 --- a/src/test/isolation/expected/eval-plan-qual.out +++ b/src/test/isolation/expected/eval-plan-qual.out @@ -144,3 +144,22 @@ accountid balance checking 1050 savings 600 + +starting permutation: updateforss readforss c1 c2 +step updateforss: + UPDATE table_a SET value = 'newTableAValue' WHERE id = 1; + UPDATE table_b SET value = 'newTableBValue' WHERE id = 1; + +step readforss: + SELECT ta.id AS ta_id, ta.value AS ta_value, + (SELECT ROW(tb.id, tb.value) + FROM table_b tb WHERE ta.id = tb.id) AS tb_row + FROM table_a ta + WHERE ta.id = 1 FOR UPDATE OF ta; + +step c1: COMMIT; +step readforss: <... completed> +ta_id ta_value tb_row + +1 newTableAValue (1,tableBValue) +step c2: COMMIT; diff --git a/src/test/isolation/specs/eval-plan-qual.spec b/src/test/isolation/specs/eval-plan-qual.spec index a391466722..de481a3cec 100644 --- a/src/test/isolation/specs/eval-plan-qual.spec +++ b/src/test/isolation/specs/eval-plan-qual.spec @@ -16,12 +16,18 @@ setup INSERT INTO c1 SELECT 0, a / 3, a % 3 FROM generate_series(0, 9) a; INSERT INTO c2 SELECT 1, a / 3, a % 3 FROM generate_series(0, 9) a; INSERT INTO c3 SELECT 2, a / 3, a % 3 FROM generate_series(0, 9) a; + + CREATE TABLE table_a (id integer, value text); + CREATE TABLE table_b (id integer, value text); + INSERT INTO table_a VALUES (1, 'tableAValue'); + INSERT INTO table_b VALUES (1, 'tableBValue'); } teardown { DROP TABLE accounts; DROP TABLE p CASCADE; + DROP TABLE table_a, table_b; } session "s1" @@ -64,6 +70,15 @@ step "lockwithvalues" { FOR UPDATE OF a1; } +# these tests exercise EvalPlanQual with a SubLink sub-select (which should be +# unaffected by any EPQ recheck behavior in the outer query); cf bug #14034 + +step "updateforss" { + UPDATE table_a SET value = 'newTableAValue' WHERE id = 1; + UPDATE table_b SET value = 'newTableBValue' WHERE id = 1; +} + + session "s2" setup { BEGIN ISOLATION LEVEL READ COMMITTED; } step "wx2" { UPDATE accounts SET balance = balance + 450 WHERE accountid = 'checking'; } @@ -81,6 +96,13 @@ step "returningp1" { WITH u AS ( UPDATE p SET b = b WHERE a > 0 RETURNING * ) SELECT * FROM u; } +step "readforss" { + SELECT ta.id AS ta_id, ta.value AS ta_value, + (SELECT ROW(tb.id, tb.value) + FROM table_b tb WHERE ta.id = tb.id) AS tb_row + FROM table_a ta + WHERE ta.id = 1 FOR UPDATE OF ta; +} step "c2" { COMMIT; } session "s3" @@ -95,3 +117,4 @@ permutation "readp1" "writep1" "readp2" "c1" "c2" permutation "writep2" "returningp1" "c1" "c2" permutation "wx2" "partiallock" "c2" "c1" "read" permutation "wx2" "lockwithvalues" "c2" "c1" "read" +permutation "updateforss" "readforss" "c1" "c2"