356 lines
14 KiB
Python
356 lines
14 KiB
Python
# Tests for the EvalPlanQual mechanism
|
|
#
|
|
# EvalPlanQual is used in READ COMMITTED isolation level to attempt to
|
|
# re-execute UPDATE and DELETE operations against rows that were updated
|
|
# by some concurrent transaction.
|
|
|
|
setup
|
|
{
|
|
CREATE TABLE accounts (accountid text PRIMARY KEY, balance numeric not null);
|
|
INSERT INTO accounts VALUES ('checking', 600), ('savings', 600);
|
|
|
|
CREATE FUNCTION update_checking(int) RETURNS bool LANGUAGE sql AS $$
|
|
UPDATE accounts SET balance = balance + 1 WHERE accountid = 'checking'; SELECT true;$$;
|
|
|
|
CREATE TABLE accounts_ext (accountid text PRIMARY KEY, balance numeric not null, other text);
|
|
INSERT INTO accounts_ext VALUES ('checking', 600, 'other'), ('savings', 700, null);
|
|
ALTER TABLE accounts_ext ADD COLUMN newcol int DEFAULT 42;
|
|
ALTER TABLE accounts_ext ADD COLUMN newcol2 text DEFAULT NULL;
|
|
|
|
CREATE TABLE p (a int, b int, c int);
|
|
CREATE TABLE c1 () INHERITS (p);
|
|
CREATE TABLE c2 () INHERITS (p);
|
|
CREATE TABLE c3 () INHERITS (p);
|
|
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');
|
|
|
|
CREATE TABLE jointest AS SELECT generate_series(1,10) AS id, 0 AS data;
|
|
CREATE INDEX ON jointest(id);
|
|
|
|
CREATE TABLE parttbl (a int, b int, c int) PARTITION BY LIST (a);
|
|
CREATE TABLE parttbl1 PARTITION OF parttbl FOR VALUES IN (1);
|
|
CREATE TABLE parttbl2 PARTITION OF parttbl FOR VALUES IN (2);
|
|
INSERT INTO parttbl VALUES (1, 1, 1);
|
|
|
|
CREATE TABLE another_parttbl (a int, b int, c int) PARTITION BY LIST (a);
|
|
CREATE TABLE another_parttbl1 PARTITION OF another_parttbl FOR VALUES IN (1);
|
|
CREATE TABLE another_parttbl2 PARTITION OF another_parttbl FOR VALUES IN (2);
|
|
INSERT INTO another_parttbl VALUES (1, 1, 1);
|
|
|
|
CREATE FUNCTION noisy_oper(p_comment text, p_a anynonarray, p_op text, p_b anynonarray)
|
|
RETURNS bool LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
r bool;
|
|
BEGIN
|
|
EXECUTE format('SELECT $1 %s $2', p_op) INTO r USING p_a, p_b;
|
|
RAISE NOTICE '%: % % % % %: %', p_comment, pg_typeof(p_a), p_a, p_op, pg_typeof(p_b), p_b, r;
|
|
RETURN r;
|
|
END;$$;
|
|
}
|
|
|
|
teardown
|
|
{
|
|
DROP TABLE accounts;
|
|
DROP FUNCTION update_checking(int);
|
|
DROP TABLE accounts_ext;
|
|
DROP TABLE p CASCADE;
|
|
DROP TABLE table_a, table_b, jointest;
|
|
DROP TABLE parttbl;
|
|
DROP TABLE another_parttbl;
|
|
DROP FUNCTION noisy_oper(text, anynonarray, text, anynonarray)
|
|
}
|
|
|
|
session "s1"
|
|
setup { BEGIN ISOLATION LEVEL READ COMMITTED; }
|
|
# wx1 then wx2 checks the basic case of re-fetching up-to-date values
|
|
step "wx1" { UPDATE accounts SET balance = balance - 200 WHERE accountid = 'checking' RETURNING balance; }
|
|
# wy1 then wy2 checks the case where quals pass then fail
|
|
step "wy1" { UPDATE accounts SET balance = balance + 500 WHERE accountid = 'checking' RETURNING balance; }
|
|
|
|
step "wxext1" { UPDATE accounts_ext SET balance = balance - 200 WHERE accountid = 'checking' RETURNING balance; }
|
|
step "tocds1" { UPDATE accounts SET accountid = 'cds' WHERE accountid = 'checking'; }
|
|
step "tocdsext1" { UPDATE accounts_ext SET accountid = 'cds' WHERE accountid = 'checking'; }
|
|
|
|
# d1 then wx1 checks that update can deal with the updated row vanishing
|
|
# wx2 then d1 checks that the delete affects the updated row
|
|
# wx2, wx2 then d1 checks that the delete checks the quals correctly (balance too high)
|
|
# wx2, d2, then d1 checks that delete handles a vanishing row correctly
|
|
step "d1" { DELETE FROM accounts WHERE accountid = 'checking' AND balance < 1500 RETURNING balance; }
|
|
|
|
# upsert tests are to check writable-CTE cases
|
|
step "upsert1" {
|
|
WITH upsert AS
|
|
(UPDATE accounts SET balance = balance + 500
|
|
WHERE accountid = 'savings'
|
|
RETURNING accountid)
|
|
INSERT INTO accounts SELECT 'savings', 500
|
|
WHERE NOT EXISTS (SELECT 1 FROM upsert);
|
|
}
|
|
|
|
# tests with table p check inheritance cases:
|
|
# readp1/writep1/readp2 tests a bug where nodeLockRows did the wrong thing
|
|
# when the first updated tuple was in a non-first child table.
|
|
# writep2/returningp1 tests a memory allocation issue
|
|
|
|
step "readp1" { SELECT tableoid::regclass, ctid, * FROM p WHERE b IN (0, 1) AND c = 0 FOR UPDATE; }
|
|
step "writep1" { UPDATE p SET b = -1 WHERE a = 1 AND b = 1 AND c = 0; }
|
|
step "writep2" { UPDATE p SET b = -b WHERE a = 1 AND c = 0; }
|
|
step "c1" { COMMIT; }
|
|
step "r1" { ROLLBACK; }
|
|
|
|
# these tests are meant to exercise EvalPlanQualFetchRowMark,
|
|
# ie, handling non-locked tables in an EvalPlanQual recheck
|
|
|
|
step "partiallock" {
|
|
SELECT * FROM accounts a1, accounts a2
|
|
WHERE a1.accountid = a2.accountid
|
|
FOR UPDATE OF a1;
|
|
}
|
|
step "lockwithvalues" {
|
|
-- Reference rowmark column that differs in type from targetlist at some attno.
|
|
-- See CAHU7rYZo_C4ULsAx_LAj8az9zqgrD8WDd4hTegDTMM1LMqrBsg@mail.gmail.com
|
|
SELECT a1.*, v.id FROM accounts a1, (values('checking'::text, 'nan'::text),('savings', 'nan')) v(id, notnumeric)
|
|
WHERE a1.accountid = v.id AND v.notnumeric != 'einszwei'
|
|
FOR UPDATE OF a1;
|
|
}
|
|
step "partiallock_ext" {
|
|
SELECT * FROM accounts_ext a1, accounts_ext a2
|
|
WHERE a1.accountid = a2.accountid
|
|
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;
|
|
}
|
|
|
|
# these tests exercise EvalPlanQual with conditional InitPlans which
|
|
# have not been executed prior to the EPQ
|
|
|
|
step "updateforcip" {
|
|
UPDATE table_a SET value = NULL WHERE id = 1;
|
|
}
|
|
|
|
# these tests exercise mark/restore during EPQ recheck, cf bug #15032
|
|
|
|
step "selectjoinforupdate" {
|
|
set local enable_nestloop to 0;
|
|
set local enable_hashjoin to 0;
|
|
set local enable_seqscan to 0;
|
|
explain (costs off)
|
|
select * from jointest a join jointest b on a.id=b.id for update;
|
|
select * from jointest a join jointest b on a.id=b.id for update;
|
|
}
|
|
|
|
# these tests exercise Result plan nodes participating in EPQ
|
|
|
|
step "selectresultforupdate" {
|
|
select * from (select 1 as x) ss1 join (select 7 as y) ss2 on true
|
|
left join table_a a on a.id = x, jointest jt
|
|
where jt.id = y;
|
|
explain (verbose, costs off)
|
|
select * from (select 1 as x) ss1 join (select 7 as y) ss2 on true
|
|
left join table_a a on a.id = x, jointest jt
|
|
where jt.id = y for update of jt, ss1, ss2;
|
|
select * from (select 1 as x) ss1 join (select 7 as y) ss2 on true
|
|
left join table_a a on a.id = x, jointest jt
|
|
where jt.id = y for update of jt, ss1, ss2;
|
|
}
|
|
|
|
# test for EPQ on a partitioned result table
|
|
|
|
step "simplepartupdate" {
|
|
update parttbl set a = a;
|
|
}
|
|
|
|
# test scenarios where update may cause row movement
|
|
|
|
step "simplepartupdate_route1to2" {
|
|
update parttbl set a = 2 where c = 1 returning *;
|
|
}
|
|
|
|
step "simplepartupdate_noroute" {
|
|
update parttbl set b = 2 where c = 1 returning *;
|
|
}
|
|
|
|
|
|
session "s2"
|
|
setup { BEGIN ISOLATION LEVEL READ COMMITTED; }
|
|
step "wx2" { UPDATE accounts SET balance = balance + 450 WHERE accountid = 'checking' RETURNING balance; }
|
|
step "wy2" { UPDATE accounts SET balance = balance + 1000 WHERE accountid = 'checking' AND balance < 1000 RETURNING balance; }
|
|
step "d2" { DELETE FROM accounts WHERE accountid = 'checking'; }
|
|
|
|
step "upsert2" {
|
|
WITH upsert AS
|
|
(UPDATE accounts SET balance = balance + 1234
|
|
WHERE accountid = 'savings'
|
|
RETURNING accountid)
|
|
INSERT INTO accounts SELECT 'savings', 1234
|
|
WHERE NOT EXISTS (SELECT 1 FROM upsert);
|
|
}
|
|
step "wx2_ext" { UPDATE accounts_ext SET balance = balance + 450; }
|
|
step "readp2" { SELECT tableoid::regclass, ctid, * FROM p WHERE b IN (0, 1) AND c = 0 FOR UPDATE; }
|
|
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 "updateforcip2" {
|
|
UPDATE table_a SET value = COALESCE(value, (SELECT text 'newValue')) WHERE id = 1;
|
|
}
|
|
step "updateforcip3" {
|
|
WITH d(val) AS (SELECT text 'newValue' FROM generate_series(1,1))
|
|
UPDATE table_a SET value = COALESCE(value, (SELECT val FROM d)) WHERE id = 1;
|
|
}
|
|
step "wrtwcte" { UPDATE table_a SET value = 'tableAValue2' WHERE id = 1; }
|
|
step "wrjt" { UPDATE jointest SET data = 42 WHERE id = 7; }
|
|
step "complexpartupdate" {
|
|
with u as (update parttbl set a = a returning parttbl.*)
|
|
update parttbl set a = u.a from u;
|
|
}
|
|
|
|
step "complexpartupdate_route_err1" {
|
|
with u as (update another_parttbl set a = 1 returning another_parttbl.*)
|
|
update parttbl p set a = u.a from u where p.a = u.a and p.c = 1 returning p.*;
|
|
}
|
|
|
|
step "complexpartupdate_route" {
|
|
with u as (update another_parttbl set a = 1 returning another_parttbl.*)
|
|
update parttbl p set a = p.b from u where p.a = u.a and p.c = 1 returning p.*;
|
|
}
|
|
|
|
step "complexpartupdate_doesnt_route" {
|
|
with u as (update another_parttbl set a = 1 returning another_parttbl.*)
|
|
update parttbl p set a = 3 - p.b from u where p.a = u.a and p.c = 1 returning p.*;
|
|
}
|
|
|
|
# Use writable CTEs to create self-updated rows, that then are
|
|
# (updated|deleted). The *fail versions of the tests additionally
|
|
# perform an update, via a function, in a different command, to test
|
|
# behaviour relating to that.
|
|
step "updwcte" { WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *) UPDATE accounts a SET balance = doup.balance + 100 FROM doup RETURNING *; }
|
|
step "updwctefail" { WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *, update_checking(999)) UPDATE accounts a SET balance = doup.balance + 100 FROM doup RETURNING *; }
|
|
step "delwcte" { WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *) DELETE FROM accounts a USING doup RETURNING *; }
|
|
step "delwctefail" { WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *, update_checking(999)) DELETE FROM accounts a USING doup RETURNING *; }
|
|
|
|
# Check that nested EPQ works correctly
|
|
step "wnested2" {
|
|
UPDATE accounts SET balance = balance - 1200
|
|
WHERE noisy_oper('upid', accountid, '=', 'checking')
|
|
AND noisy_oper('up', balance, '>', 200.0)
|
|
AND EXISTS (
|
|
SELECT accountid
|
|
FROM accounts_ext ae
|
|
WHERE noisy_oper('lock_id', ae.accountid, '=', accounts.accountid)
|
|
AND noisy_oper('lock_bal', ae.balance, '>', 200.0)
|
|
FOR UPDATE
|
|
);
|
|
}
|
|
|
|
step "c2" { COMMIT; }
|
|
step "r2" { ROLLBACK; }
|
|
|
|
session "s3"
|
|
setup { BEGIN ISOLATION LEVEL READ COMMITTED; }
|
|
step "read" { SELECT * FROM accounts ORDER BY accountid; }
|
|
step "read_ext" { SELECT * FROM accounts_ext ORDER BY accountid; }
|
|
step "read_a" { SELECT * FROM table_a ORDER BY id; }
|
|
|
|
# this test exercises EvalPlanQual with a CTE, cf bug #14328
|
|
step "readwcte" {
|
|
WITH
|
|
cte1 AS (
|
|
SELECT id FROM table_b WHERE value = 'tableBValue'
|
|
),
|
|
cte2 AS (
|
|
SELECT * FROM table_a
|
|
WHERE id = (SELECT id FROM cte1)
|
|
FOR UPDATE
|
|
)
|
|
SELECT * FROM cte2;
|
|
}
|
|
|
|
# this test exercises a different CTE misbehavior, cf bug #14870
|
|
step "multireadwcte" {
|
|
WITH updated AS (
|
|
UPDATE table_a SET value = 'tableAValue3' WHERE id = 1 RETURNING id
|
|
)
|
|
SELECT (SELECT id FROM updated) AS subid, * FROM updated;
|
|
}
|
|
|
|
teardown { COMMIT; }
|
|
|
|
# test that normal update follows update chains, and reverifies quals
|
|
permutation "wx1" "wx2" "c1" "c2" "read"
|
|
permutation "wy1" "wy2" "c1" "c2" "read"
|
|
permutation "wx1" "wx2" "r1" "c2" "read"
|
|
permutation "wy1" "wy2" "r1" "c2" "read"
|
|
|
|
# test that deletes follow chains, and if necessary reverifies quals
|
|
permutation "wx1" "d1" "wx2" "c1" "c2" "read"
|
|
permutation "wx2" "d1" "c2" "c1" "read"
|
|
permutation "wx2" "wx2" "d1" "c2" "c1" "read"
|
|
permutation "wx2" "d2" "d1" "c2" "c1" "read"
|
|
permutation "wx1" "d1" "wx2" "r1" "c2" "read"
|
|
permutation "wx2" "d1" "r2" "c1" "read"
|
|
permutation "wx2" "wx2" "d1" "r2" "c1" "read"
|
|
permutation "wx2" "d2" "d1" "r2" "c1" "read"
|
|
permutation "d1" "wx2" "c1" "c2" "read"
|
|
permutation "d1" "wx2" "r1" "c2" "read"
|
|
|
|
# Check that nested EPQ works correctly
|
|
permutation "wnested2" "c1" "c2" "read"
|
|
permutation "wx1" "wxext1" "wnested2" "c1" "c2" "read"
|
|
permutation "wx1" "wx1" "wxext1" "wnested2" "c1" "c2" "read"
|
|
permutation "wx1" "wx1" "wxext1" "wxext1" "wnested2" "c1" "c2" "read"
|
|
permutation "wx1" "wxext1" "wxext1" "wnested2" "c1" "c2" "read"
|
|
permutation "wx1" "tocds1" "wnested2" "c1" "c2" "read"
|
|
permutation "wx1" "tocdsext1" "wnested2" "c1" "c2" "read"
|
|
|
|
# test that an update to a self-modified row is ignored when
|
|
# previously updated by the same cid
|
|
permutation "wx1" "updwcte" "c1" "c2" "read"
|
|
# test that an update to a self-modified row throws error when
|
|
# previously updated by a different cid
|
|
permutation "wx1" "updwctefail" "c1" "c2" "read"
|
|
# test that a delete to a self-modified row is ignored when
|
|
# previously updated by the same cid
|
|
permutation "wx1" "delwcte" "c1" "c2" "read"
|
|
# test that a delete to a self-modified row throws error when
|
|
# previously updated by a different cid
|
|
permutation "wx1" "delwctefail" "c1" "c2" "read"
|
|
|
|
permutation "upsert1" "upsert2" "c1" "c2" "read"
|
|
permutation "readp1" "writep1" "readp2" "c1" "c2"
|
|
permutation "writep2" "returningp1" "c1" "c2"
|
|
permutation "wx2" "partiallock" "c2" "c1" "read"
|
|
permutation "wx2" "lockwithvalues" "c2" "c1" "read"
|
|
permutation "wx2_ext" "partiallock_ext" "c2" "c1" "read_ext"
|
|
permutation "updateforss" "readforss" "c1" "c2"
|
|
permutation "updateforcip" "updateforcip2" "c1" "c2" "read_a"
|
|
permutation "updateforcip" "updateforcip3" "c1" "c2" "read_a"
|
|
permutation "wrtwcte" "readwcte" "c1" "c2"
|
|
permutation "wrjt" "selectjoinforupdate" "c2" "c1"
|
|
permutation "wrjt" "selectresultforupdate" "c2" "c1"
|
|
permutation "wrtwcte" "multireadwcte" "c1" "c2"
|
|
|
|
permutation "simplepartupdate" "complexpartupdate" "c1" "c2"
|
|
permutation "simplepartupdate_route1to2" "complexpartupdate_route_err1" "c1" "c2"
|
|
permutation "simplepartupdate_noroute" "complexpartupdate_route" "c1" "c2"
|
|
permutation "simplepartupdate_noroute" "complexpartupdate_doesnt_route" "c1" "c2"
|