# 2015 July 26 # # The author disclaims copyright to this source code. In place of # a legal notice, here is a blessing: # # May you do good and not evil. # May you find forgiveness for yourself and forgive others. # May you share freely, never taking more than you give. # #*********************************************************************** # set testdir [file dirname $argv0] source $testdir/tester.tcl source $testdir/lock_common.tcl set ::testprefix concurrent ifcapable !concurrent { finish_test return } do_execsql_test 1.0 { PRAGMA journal_mode = wal; } {wal} do_execsql_test 1.1 { CREATE TABLE t1(k INTEGER PRIMARY KEY, v); BEGIN CONCURRENT; INSERT INTO t1 VALUES(1, 'abcd'); COMMIT; } do_execsql_test 1.2 { SELECT * FROM t1; } {1 abcd} do_execsql_test 1.3 { BEGIN CONCURRENT; INSERT INTO t1 VALUES(2, 'efgh'); ROLLBACK; } do_execsql_test 1.4 { SELECT * FROM t1; } {1 abcd} #------------------------------------------------------------------------- # CONCURRENT transactions cannot do cache spills. # foreach {tn trans spill} { 1 {BEGIN CONCURRENT} 0 2 {BEGIN} 1 } { do_test 1.5.$tn { sqlite3 db2 test.db set walsz [file size test.db-wal] execsql { PRAGMA cache_size = 10 } db2 execsql $trans db2 execsql { WITH cnt(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM cnt WHERE i<50) INSERT INTO t1(v) SELECT randomblob(900) FROM cnt; } db2 expr {[file size test.db-wal]==$walsz} } [expr !$spill] execsql ROLLBACK db2 db2 close } #------------------------------------------------------------------------- # CONCURRENT transactions man not be committed while there are active # readers. do_execsql_test 1.6.setup { DROP TABLE t1; CREATE TABLE t1(a, b); INSERT INTO t1 VALUES(1, 2); INSERT INTO t1 VALUES(3, 4); INSERT INTO t1 VALUES(5, 6); } foreach {tn trans commit_ok} { 1 {BEGIN CONCURRENT} 0 2 {BEGIN} 1 } { do_test 1.6.$tn.1 { set stmt [sqlite3_prepare db "SELECT * FROM t1" -1 dummy] sqlite3_step $stmt } SQLITE_ROW do_test 1.6.$tn.2 { execsql $trans execsql { INSERT INTO t1 VALUES(7, 8) } } {} if { $commit_ok } { do_test 1.6.$tn.3 { catchsql COMMIT } {0 {}} } else { do_test 1.6.$tn.4 { catchsql COMMIT } {/1 {cannot commit transaction .*}/} } sqlite3_finalize $stmt catchsql ROLLBACK } #------------------------------------------------------------------------- # CONCURRENT transactions may not modify the db schema. # sqlite3 db2 test.db foreach {tn sql} { 1 { CREATE TABLE xx(a, b) } 2 { DROP TABLE t1 } 3 { CREATE INDEX i1 ON t1(a) } 4 { CREATE VIEW v1 AS SELECT * FROM t1 } } { do_catchsql_test 1.7.0.$tn.1 " BEGIN CONCURRENT; $sql " {0 {}} db2 eval {INSERT INTO t1 DEFAULT VALUES} do_catchsql_test 1.7.0.$tn.2 { COMMIT } {1 {database is locked}} do_execsql_test 1.7.0.$tn.2 ROLLBACK do_execsql_test 1.7.0.$tn.3 { SELECT sql FROM sqlite_master; SELECT sql FROM sqlite_temp_master; } {{CREATE TABLE t1(a, b)}} #do_execsql_test 1.7.0.$tn.3 COMMIT } # Except the temp db schema. foreach {tn sql} { 1 { CREATE TEMP TABLE xx(a, b) } 2 { DROP TABLE xx } 3 { CREATE TEMP TABLE yy(a, b) } 4 { CREATE VIEW temp.v1 AS SELECT * FROM t1 } 5 { CREATE INDEX yyi1 ON yy(a); } 6 { CREATE TABLE temp.zz(a, b) } } { do_catchsql_test 1.7.1.$tn.1 " BEGIN CONCURRENT; $sql " {0 {}} do_execsql_test 1.7.1.$tn.2 COMMIT } do_execsql_test 1.7.1.x { SELECT sql FROM sqlite_master; SELECT sql FROM sqlite_temp_master; } { {CREATE TABLE t1(a, b)} {CREATE TABLE yy(a, b)} {CREATE VIEW v1 AS SELECT * FROM t1} {CREATE INDEX yyi1 ON yy(a)} {CREATE TABLE zz(a, b)} } db2 close #------------------------------------------------------------------------- # If an auto-vacuum database is written within an CONCURRENT transaction, it # is handled in the same way as for a non-CONCURRENT transaction. # reset_db do_execsql_test 1.8.1 { PRAGMA auto_vacuum = 1; PRAGMA journal_mode = wal; CREATE TABLE t1(x, y); INSERT INTO t1 VALUES('x', 'y'); } {wal} do_execsql_test 1.8.2 { BEGIN CONCURRENT; SELECT * FROM t1; COMMIT; } {x y} do_catchsql_test 1.8.3 { BEGIN CONCURRENT; INSERT INTO t1 VALUES('a', 'b'); } {0 {}} do_test 1.8.4 { sqlite3 db2 test.db catchsql { BEGIN CONCURRENT; INSERT INTO t1 VALUES('c', 'd'); } db2 } {1 {database is locked}} do_test 1.8.5 { db eval COMMIT db2 eval COMMIT } {} db close db2 close do_multiclient_test tn { #----------------------------------------------------------------------- # 1. Start an CONCURRENT transaction using [db1]. # # 2. Start and then rollback a regular transaction using [db2]. This # can be done as the ongoing [db1] transaction is CONCURRENT. # # 3. The [db1] transaction can now be committed, as [db2] has relinquished # the write lock. # do_test 2.$tn.1.1 { sql1 { PRAGMA journal_mode = wal; CREATE TABLE t1(k INTEGER PRIMARY KEY, v); INSERT INTO t1 VALUES(1, 'one'); } sql1 { BEGIN CONCURRENT; INSERT INTO t1 VALUES(2, 'two'); } code1 { sqlite3_get_autocommit db } } 0 do_test 2.$tn.1.2 { sql2 { BEGIN; INSERT INTO t1 VALUES(3, 'three'); ROLLBACK; } } {} do_test 2.$tn.1.3 { sql1 COMMIT sql2 { SELECT * FROM t1 } } {1 one 2 two} #----------------------------------------------------------------------- # 1. Start an CONCURRENT transaction using [db1]. # # 2. Commit a transaction using [db2]. # # 3. Try to commit with [db1]. Check that SQLITE_BUSY_SNAPSHOT is returned, # and the transaction is not rolled back. # do_test 2.$tn.2.1 { sql1 { BEGIN CONCURRENT; INSERT INTO t1 VALUES(-1, 'hello world'); } } {} do_test 2.$tn.2.2 { sql2 { INSERT INTO t1 VALUES(3, 'three'); } } {} do_test 2.$tn.2.3.1 { set rc [catch { sql1 COMMIT } msg] list $rc $msg } {1 {database is locked}} do_test 2.$tn.2.3.2 { code1 { list [sqlite3_extended_errcode db] [sqlite3_get_autocommit db] } } {SQLITE_BUSY_SNAPSHOT 0} do_test 2.$tn.2.3.3 { sql1 { SELECT * FROM t1; ROLLBACK; } } {-1 {hello world} 1 one 2 two} #----------------------------------------------------------------------- # 1. Start an CONCURRENT transaction using [db1]. # # 2. Open a transaction using [db2]. # # 3. Try to commit with [db1]. Check that SQLITE_BUSY is returned, # and the transaction is not rolled back. # # 4. Have [db2] roll its transaction back. Then check that [db1] can # commit. # do_test 2.$tn.3.1 { sql1 { BEGIN CONCURRENT; INSERT INTO t1 VALUES(4, 'four'); } } {} do_test 2.$tn.3.2 { sql2 { BEGIN; INSERT INTO t1 VALUES(-1, 'xyz'); } } {} do_test 2.$tn.3.3.1 { set rc [catch { sql1 COMMIT } msg] list $rc $msg } {1 {database is locked}} do_test 2.$tn.3.3.2 { code1 { list [sqlite3_extended_errcode db] [sqlite3_get_autocommit db] } } {SQLITE_BUSY 0} do_test 2.$tn.3.3.3 { sql1 { SELECT * FROM t1; } } {1 one 2 two 3 three 4 four} do_test 2.$tn.3.4 { sql2 ROLLBACK sql1 COMMIT sql1 { SELECT * FROM t1; } } {1 one 2 two 3 three 4 four} #----------------------------------------------------------------------- # 1. Create a second table - t2. # # 2. Write to t1 with [db] and t2 with [db2]. # # 3. See if it worked. # do_test 2.$tn.4.1 { sql1 { CREATE TABLE t2(a, b) } } {} do_test 2.$tn.4.2 { sql2 { BEGIN CONCURRENT; INSERT INTO t2 VALUES('i', 'n'); } sql1 { BEGIN CONCURRENT; INSERT INTO t1 VALUES(5, 'five'); COMMIT; } sql2 COMMIT } {} do_test 2.$tn.4.3.1 { sql2 {SELECT * FROM t1} } {1 one 2 two 3 three 4 four 5 five} do_test 2.$tn.4.3.2 { sql1 {SELECT * FROM t1} } {1 one 2 two 3 three 4 four 5 five} do_test 2.$tn.4.3.3 { sql2 {SELECT * FROM t2} } {i n} do_test 2.$tn.4.3.4 { sql1 {SELECT * FROM t2} } {i n} #----------------------------------------------------------------------- # The "schema cookie" issue. # # 1. Begin and CONCURRENT write to "t1" using [db] # # 2. Create an index on t1 using [db2]. # # 3. Attempt to commit the CONCURRENT write. This is an SQLITE_BUSY_SNAPSHOT, # even though there is no page collision. # do_test 2.$tn.5.1 { sql1 { BEGIN CONCURRENT; INSERT INTO t1 VALUES(6, 'six'); } } {} do_test 2.$tn.5.2 { sql2 { CREATE INDEX i1 ON t1(v); } } {} do_test 2.$tn.5.3 { list [catch { sql1 { COMMIT } } msg] $msg [sqlite3_errcode db] } {1 {database is locked} SQLITE_BUSY_SNAPSHOT} do_test 2.$tn.5.4 { sql2 { PRAGMA integrity_check } } {ok} catch { sql1 ROLLBACK } #----------------------------------------------------------------------- # # 1. Begin an CONCURRENT write to "t1" using [db] # # 2. Lots of inserts into t2. Enough to grow the db file and modify page 1. # # 3. Check that the CONCURRENT transaction can not be committed. # do_test 2.$tn.6.1 { sql1 { BEGIN CONCURRENT; INSERT INTO t1 VALUES(6, 'six'); } } {} do_test 2.$tn.6.2 { sql2 { WITH src(a,b) AS ( VALUES(1,1) UNION ALL SELECT a+1,b+1 FROM src WHERE a<10000 ) INSERT INTO t2 SELECT * FROM src; } } {} do_test 2.$tn.6.3 { sql1 { SELECT count(*) FROM t2 } list [catch { sql1 { COMMIT } } msg] $msg [sqlite3_errcode db] } {1 {database is locked} SQLITE_BUSY_SNAPSHOT} sql1 ROLLBACK do_test 2.$tn.6.4 { sql1 { SELECT count(*) FROM t1; SELECT count(*) FROM t2; } } {5 10001} #----------------------------------------------------------------------- # # 1. Begin an big CONCURRENT write to "t1" using [db] - large enough to # grow the db file. # # 2. Lots of inserts into t2. Also enough to grow the db file. # # 3. Check that the CONCURRENT transaction cannot be committed (due to a clash # on page 1 - the db size field). # do_test 2.$tn.7.1 { sql1 { BEGIN CONCURRENT; WITH src(a,b) AS ( VALUES(10000,10000) UNION ALL SELECT a+1,b+1 FROM src WHERE a<20000 ) INSERT INTO t1 SELECT * FROM src; } } {} do_test 2.$tn.7.2 { sql2 { WITH src(a,b) AS ( VALUES(1,1) UNION ALL SELECT a+1,b+1 FROM src WHERE a<10000 ) INSERT INTO t2 SELECT * FROM src; } } {} do_test 2.$tn.7.3 { list [catch { sql1 { COMMIT } } msg] $msg [sqlite3_errcode db] } {0 {} SQLITE_OK} do_test 2.$tn.7.4 { sql3 { PRAGMA integrity_check } } ok } #------------------------------------------------------------------------- # Concurrent transactions may not modify the user_version or application_id. # reset_db do_execsql_test 3.0 { PRAGMA journal_mode = wal; CREATE TABLE t1(x, y); INSERT INTO t1 VALUES('a', 'b'); PRAGMA user_version = 10; } {wal} do_execsql_test 3.1 { BEGIN CONCURRENT; INSERT INTO t1 VALUES('c', 'd'); SELECT * FROM t1; } {a b c d} do_catchsql_test 3.2 { PRAGMA user_version = 11; } {1 {cannot modify user_version within CONCURRENT transaction}} do_execsql_test 3.3 { PRAGMA user_version; SELECT * FROM t1; } {10 a b c d} do_catchsql_test 3.4 { PRAGMA application_id = 11; } {1 {cannot modify application_id within CONCURRENT transaction}} do_execsql_test 3.5 { COMMIT; PRAGMA user_version; PRAGMA application_id; SELECT * FROM t1; } {10 0 a b c d} #------------------------------------------------------------------------- # However, another transaction modifying the user_version or application_id # should not cause a conflict. And committing a concurrent transaction does not # clobber the modification - even if the concurrent transaction allocates or # frees database pages. # do_multiclient_test tn { do_test 4.$tn.1 { sql1 { PRAGMA journal_mode = wal; CREATE TABLE ttt(y UNIQUE, z UNIQUE); PRAGMA user_version = 14; BEGIN CONCURRENT; INSERT INTO ttt VALUES('y', 'z'); } } {wal} do_test 4.$tn.2 { sql2 { PRAGMA user_version = 16 } sql1 COMMIT sql1 { PRAGMA user_version } } {16} do_test 4.$tn.3 { sql1 { BEGIN CONCURRENT; INSERT INTO ttt VALUES(randomblob(10000), randomblob(4)); PRAGMA user_version; } } {16} do_test 4.$tn.4 { sql2 { PRAGMA user_version = 1234 } sql1 { PRAGMA user_version; COMMIT; PRAGMA user_version; PRAGMA integrity_check; } } {16 1234 ok} do_test 4.$tn.5 { sql1 { BEGIN CONCURRENT; DELETE FROM ttt; PRAGMA user_version; } } {1234} do_test 4.$tn.4 { sql2 { PRAGMA user_version = 5678 } sql1 { PRAGMA user_version; COMMIT; PRAGMA user_version; PRAGMA integrity_check; } } {1234 5678 ok} } do_multiclient_test tn { do_test 5.$tn.1 { sql1 { PRAGMA journal_mode = wal; CREATE TABLE tt(a INTEGER PRIMARY KEY, b); CREATE TABLE t2(a INTEGER PRIMARY KEY, b); INSERT INTO tt VALUES(1, randomblob(400)); BEGIN CONCURRENT; } } {wal} do_test 5.$tn.2 { sql1 { UPDATE t2 SET b=5 WHERE a=3 } sql2 { INSERT INTO tt VALUES(2, randomblob(6000)) } } {} do_test 5.$tn.3 { sql1 { COMMIT } } {} } do_multiclient_test tn { do_test 6.$tn.1 { sql1 { PRAGMA journal_mode = wal; CREATE TABLE t1(a INTEGER PRIMARY KEY, b); CREATE TABLE t2(a INTEGER PRIMARY KEY, b); INSERT INTO t1 VALUES(1, 'one'); INSERT INTO t2 VALUES(2, 'two'); } } {wal} do_test 6.$tn.2 { sql2 { BEGIN CONCURRENT; SELECT * FROM t2; INSERT INTO t1 VALUES(3, 'three'); } } {2 two} do_test 6.$tn.3 { sql1 { INSERT INTO t2 VALUES(3, 'three'); } } {} do_test 6.$tn.2 { list [catch { sql2 { COMMIT } } msg] $msg } {1 {database is locked}} } do_multiclient_test tn { do_test 7.$tn.1 { sql1 { PRAGMA journal_mode = wal; CREATE TABLE t1(a INTEGER PRIMARY KEY, b); WITH s(i) AS ( VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<100) INSERT INTO t1 SELECT NULL, randomblob(400) FROM s; CREATE TABLE t2(a INTEGER PRIMARY KEY, b); WITH s(i) AS ( VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<50000) INSERT INTO t2 SELECT NULL, randomblob(400) FROM s; CREATE TABLE t3(a INTEGER PRIMARY KEY, b); WITH s(i) AS ( VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<100) INSERT INTO t3 SELECT NULL, randomblob(400) FROM s; CREATE TABLE t4(a INTEGER PRIMARY KEY, b); } set {} {} } {} do_test 7.$tn.2 { sql2 { BEGIN CONCURRENT; SELECT * FROM t1; INSERT INTO t4 VALUES(1, 2); } set {} {} } {} do_test 7.$tn.3 { sql3 { BEGIN CONCURRENT; SELECT * FROM t3; INSERT INTO t4 VALUES(1, 2); } set {} {} } {} do_test 7.$tn.4 { sql1 { UPDATE t1 SET b=randomblob(400); UPDATE t2 SET b=randomblob(400); UPDATE t3 SET b=randomblob(400); } } {} do_test 7.$tn.5 { csql2 { COMMIT } } {1 {database is locked}} do_test 7.$tn.6 { csql3 { COMMIT } } {1 {database is locked}} csql2 ROLLBACK csql3 ROLLBACK # The following test works with $tn==1 (sql2 and sql3 use separate # processes), but is quite slow. So only run it with $tn==2 (all # connections in the same process). # if {$tn==2} { do_test 7.$tn.7 { for {set i 1} {$i < 10000} {incr i} { sql3 { PRAGMA wal_checkpoint; BEGIN CONCURRENT; SELECT * FROM t3; INSERT INTO t4 VALUES(1, 2); } sql1 { UPDATE t2 SET b = randomblob(400) WHERE rowid <= $i; UPDATE t3 SET b = randomblob(400) WHERE rowid = 1; } if {[csql3 COMMIT]!={1 {database is locked}}} { error "Failed at i=$i" } csql3 ROLLBACK } } {} } } finish_test