diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c index e3c55eb..c954cfc 100644 --- a/src/backend/access/nbtree/nbtinsert.c +++ b/src/backend/access/nbtree/nbtinsert.c @@ -217,6 +217,52 @@ top: } /* + * Check if a heap tuple is still live, or committed dead. This is a waste of + * time in normal scenarios but we must do it to support CREATE INDEX + * CONCURRENTLY. + * + * We must follow HOT-chains here because during concurrent index build, we + * insert the root TID though the actual tuple may be somewhere in the + * HOT-chain. While following the chain we might not stop at the exact tuple + * which triggered the insert, but that's OK because if we find a live tuple + * anywhere in this chain, we have a unique key conflict. The other live + * tuple is not part of this chain because it had a different index entry. + * + * If the tuple is live, then also take the opportunity to check for SSI + * conflicts. + */ +static bool +check_unique_tuple_still_live(Relation irel, Buffer ibuf, Relation heapRel, + ItemPointer htid) +{ + Buffer heapBuf; + HeapTupleData heapTuple; + bool found; + + heapBuf = ReadBuffer(heapRel, + ItemPointerGetBlockNumber(htid)); + LockBuffer(heapBuf, BUFFER_LOCK_SHARE); + found = heap_hot_search_buffer(htid, heapRel, heapBuf, + SnapshotSelf, &heapTuple, + NULL, true); + if (found) + { + /* + * The caller will ereport a unique constraint violation when we + * return. Before we return, check for a conflict-in as we would if + * we were going to write to this page. We aren't going to write, + * we're going to ereport, but we want to detect any SSI conflicts + * that would arise if we did. + */ + CheckForSerializableConflictIn(irel, NULL, ibuf); + } + LockBuffer(heapBuf, BUFFER_LOCK_UNLOCK); + ReleaseBuffer(heapBuf); + + return found; +} + +/* * _bt_check_unique() -- Check for violation of unique index constraint * * offset points to the first possible item that could conflict. It can @@ -364,21 +410,9 @@ _bt_check_unique(Relation rel, IndexTuple itup, Relation heapRel, * Otherwise we have a definite conflict. But before * complaining, look to see if the tuple we want to insert * is itself now committed dead --- if so, don't complain. - * This is a waste of time in normal scenarios but we must - * do it to support CREATE INDEX CONCURRENTLY. - * - * We must follow HOT-chains here because during - * concurrent index build, we insert the root TID though - * the actual tuple may be somewhere in the HOT-chain. - * While following the chain we might not stop at the - * exact tuple which triggered the insert, but that's OK - * because if we find a live tuple anywhere in this chain, - * we have a unique key conflict. The other live tuple is - * not part of this chain because it had a different index - * entry. */ htid = itup->t_tid; - if (heap_hot_search(&htid, heapRel, SnapshotSelf, NULL)) + if (check_unique_tuple_still_live(rel, buf, heapRel, &htid)) { /* Normal case --- it's still live */ } diff --git a/src/test/isolation/expected/read-write-unique-2.out b/src/test/isolation/expected/read-write-unique-2.out new file mode 100644 index 0000000..5e27f0a --- /dev/null +++ b/src/test/isolation/expected/read-write-unique-2.out @@ -0,0 +1,29 @@ +Parsed test spec with 2 sessions + +starting permutation: r1 r2 w1 w2 c1 c2 +step r1: SELECT * FROM test WHERE i = 42; +i + +step r2: SELECT * FROM test WHERE i = 42; +i + +step w1: INSERT INTO test VALUES (42); +step w2: INSERT INTO test VALUES (42); +step c1: COMMIT; +step w2: <... completed> +error in steps c1 w2: ERROR: could not serialize access due to read/write dependencies among transactions +step c2: COMMIT; + +starting permutation: r1 w1 c1 r2 w2 c2 +step r1: SELECT * FROM test WHERE i = 42; +i + +step w1: INSERT INTO test VALUES (42); +step c1: COMMIT; +step r2: SELECT * FROM test WHERE i = 42; +i + +42 +step w2: INSERT INTO test VALUES (42); +ERROR: duplicate key value violates unique constraint "test_pkey" +step c2: COMMIT; diff --git a/src/test/isolation/expected/read-write-unique-3.out b/src/test/isolation/expected/read-write-unique-3.out new file mode 100644 index 0000000..edd3558 --- /dev/null +++ b/src/test/isolation/expected/read-write-unique-3.out @@ -0,0 +1,12 @@ +Parsed test spec with 2 sessions + +starting permutation: rw1 rw2 c1 c2 +step rw1: SELECT insert_unique(1, '1'); +insert_unique + + +step rw2: SELECT insert_unique(1, '2'); +step c1: COMMIT; +step rw2: <... completed> +error in steps c1 rw2: ERROR: could not serialize access due to read/write dependencies among transactions +step c2: COMMIT; diff --git a/src/test/isolation/expected/read-write-unique-4.out b/src/test/isolation/expected/read-write-unique-4.out new file mode 100644 index 0000000..64ff157 --- /dev/null +++ b/src/test/isolation/expected/read-write-unique-4.out @@ -0,0 +1,41 @@ +Parsed test spec with 2 sessions + +starting permutation: r1 r2 w1 w2 c1 c2 +step r1: SELECT COALESCE(MAX(invoice_number) + 1, 1) FROM invoice WHERE year = 2016; +coalesce + +3 +step r2: SELECT COALESCE(MAX(invoice_number) + 1, 1) FROM invoice WHERE year = 2016; +coalesce + +3 +step w1: INSERT INTO invoice VALUES (2016, 3); +step w2: INSERT INTO invoice VALUES (2016, 3); +step c1: COMMIT; +step w2: <... completed> +error in steps c1 w2: ERROR: could not serialize access due to read/write dependencies among transactions +step c2: COMMIT; + +starting permutation: r1 w1 w2 c1 c2 +step r1: SELECT COALESCE(MAX(invoice_number) + 1, 1) FROM invoice WHERE year = 2016; +coalesce + +3 +step w1: INSERT INTO invoice VALUES (2016, 3); +step w2: INSERT INTO invoice VALUES (2016, 3); +step c1: COMMIT; +step w2: <... completed> +error in steps c1 w2: ERROR: duplicate key value violates unique constraint "invoice_pkey" +step c2: COMMIT; + +starting permutation: r2 w1 w2 c1 c2 +step r2: SELECT COALESCE(MAX(invoice_number) + 1, 1) FROM invoice WHERE year = 2016; +coalesce + +3 +step w1: INSERT INTO invoice VALUES (2016, 3); +step w2: INSERT INTO invoice VALUES (2016, 3); +step c1: COMMIT; +step w2: <... completed> +error in steps c1 w2: ERROR: duplicate key value violates unique constraint "invoice_pkey" +step c2: COMMIT; diff --git a/src/test/isolation/expected/read-write-unique.out b/src/test/isolation/expected/read-write-unique.out new file mode 100644 index 0000000..fb32ec3 --- /dev/null +++ b/src/test/isolation/expected/read-write-unique.out @@ -0,0 +1,29 @@ +Parsed test spec with 2 sessions + +starting permutation: r1 r2 w1 w2 c1 c2 +step r1: SELECT * FROM test; +i + +step r2: SELECT * FROM test; +i + +step w1: INSERT INTO test VALUES (42); +step w2: INSERT INTO test VALUES (42); +step c1: COMMIT; +step w2: <... completed> +error in steps c1 w2: ERROR: could not serialize access due to read/write dependencies among transactions +step c2: COMMIT; + +starting permutation: r1 w1 c1 r2 w2 c2 +step r1: SELECT * FROM test; +i + +step w1: INSERT INTO test VALUES (42); +step c1: COMMIT; +step r2: SELECT * FROM test; +i + +42 +step w2: INSERT INTO test VALUES (42); +ERROR: duplicate key value violates unique constraint "test_pkey" +step c2: COMMIT; diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 138a0b7..fbd2192 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -1,3 +1,7 @@ +test: read-write-unique +test: read-write-unique-2 +test: read-write-unique-3 +test: read-write-unique-4 test: simple-write-skew test: receipt-report test: temporal-range-integrity diff --git a/src/test/isolation/specs/read-write-unique-2.spec b/src/test/isolation/specs/read-write-unique-2.spec new file mode 100644 index 0000000..5e7cbf2 --- /dev/null +++ b/src/test/isolation/specs/read-write-unique-2.spec @@ -0,0 +1,36 @@ +# Read-write-unique test. + +setup +{ + CREATE TABLE test (i integer PRIMARY KEY); +} + +teardown +{ + DROP TABLE test; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "r1" { SELECT * FROM test WHERE i = 42; } +step "w1" { INSERT INTO test VALUES (42); } +step "c1" { COMMIT; } + +session "s2" +setup { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "r2" { SELECT * FROM test WHERE i = 42; } +step "w2" { INSERT INTO test VALUES (42); } +step "c2" { COMMIT; } + +# Two SSI transactions see that there is no row with value 42 +# in the table, then try to insert that value; T1 inserts, +# and then T2 blocks waiting for T1 to commit. Finally, +# T2 reports a serialization failure. + +permutation "r1" "r2" "w1" "w2" "c1" "c2" + +# If the value is already visible before T2 begins, then a +# regular unique constraint violation should still be raised +# by T2. + +permutation "r1" "w1" "c1" "r2" "w2" "c2" diff --git a/src/test/isolation/specs/read-write-unique-3.spec b/src/test/isolation/specs/read-write-unique-3.spec new file mode 100644 index 0000000..52d2877 --- /dev/null +++ b/src/test/isolation/specs/read-write-unique-3.spec @@ -0,0 +1,33 @@ +# Read-write-unique test. +# From bug report 9301. + +setup +{ + CREATE TABLE test ( + key integer UNIQUE, + val text + ); + + CREATE OR REPLACE FUNCTION insert_unique(k integer, v text) RETURNS void + LANGUAGE SQL AS $$ + INSERT INTO test (key, val) SELECT k, v WHERE NOT EXISTS (SELECT key FROM test WHERE key = k); + $$; +} + +teardown +{ + DROP FUNCTION insert_unique(integer, text); + DROP TABLE test; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "rw1" { SELECT insert_unique(1, '1'); } +step "c1" { COMMIT; } + +session "s2" +setup { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "rw2" { SELECT insert_unique(1, '2'); } +step "c2" { COMMIT; } + +permutation "rw1" "rw2" "c1" "c2" diff --git a/src/test/isolation/specs/read-write-unique-4.spec b/src/test/isolation/specs/read-write-unique-4.spec new file mode 100644 index 0000000..e535610 --- /dev/null +++ b/src/test/isolation/specs/read-write-unique-4.spec @@ -0,0 +1,40 @@ +# Read-write-unique test. +# Implementing a gapless sequence of ID numbers for each year. + +setup +{ + CREATE TABLE invoice ( + year int, + invoice_number int, + PRIMARY KEY (year, invoice_number) + ); + + INSERT INTO invoice VALUES (2016, 1), (2016, 2); +} + +teardown +{ + DROP TABLE invoice; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "r1" { SELECT COALESCE(MAX(invoice_number) + 1, 1) FROM invoice WHERE year = 2016; } +step "w1" { INSERT INTO invoice VALUES (2016, 3); } +step "c1" { COMMIT; } + +session "s2" +setup { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "r2" { SELECT COALESCE(MAX(invoice_number) + 1, 1) FROM invoice WHERE year = 2016; } +step "w2" { INSERT INTO invoice VALUES (2016, 3); } +step "c2" { COMMIT; } + +# if they both read first then there should be an SSI conflict +permutation "r1" "r2" "w1" "w2" "c1" "c2" + +# if s2 doesn't both to read first, then inserting 3 should generate a unique constraint failure +permutation "r1" "w1" "w2" "c1" "c2" + +# if s1 doesn't both to read first, but s2 does, then s1 got lucky and s2 should still experience an SSI failure +# TODO: but in fact it still experiences a unique constraint violation... +permutation "r2" "w1" "w2" "c1" "c2" diff --git a/src/test/isolation/specs/read-write-unique.spec b/src/test/isolation/specs/read-write-unique.spec new file mode 100644 index 0000000..c782f10 --- /dev/null +++ b/src/test/isolation/specs/read-write-unique.spec @@ -0,0 +1,39 @@ +# Read-write-unique test. + +setup +{ + CREATE TABLE test (i integer PRIMARY KEY); +} + +teardown +{ + DROP TABLE test; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "r1" { SELECT * FROM test; } +step "w1" { INSERT INTO test VALUES (42); } +step "c1" { COMMIT; } + +session "s2" +setup { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "r2" { SELECT * FROM test; } +step "w2" { INSERT INTO test VALUES (42); } +step "c2" { COMMIT; } + +# Two SSI transactions see that there is no row with value 42 +# in the table, then try to insert that value; T1 inserts, +# and then T2 blocks waiting for T1 to commit. Finally, +# T2 reports a serialization failure. +# +# (In an earlier version of Postgres, T2 would report a unique +# constraint violation). + +permutation "r1" "r2" "w1" "w2" "c1" "c2" + +# If the value is already visible before T2 begins, then a +# regular unique constraint violation should still be raised +# by T2. + +permutation "r1" "w1" "c1" "r2" "w2" "c2"