import XCTest import Dispatch import Foundation @testable import GRDB class DatabasePoolConcurrencyTests: GRDBTestCase { func testDatabasePoolFundamental1() throws { // Constraint: the sum of values, the balance, must remain zero. // // This test shows (if needed) that writes that are not wrapped in a // transaction can not provide our guarantee, and that deferred // transaction provide the immutable view of the database needed by // dbPool.read(). // // Reader Writer // BEGIN DEFERRED TRANSACTION let s1 = DispatchSemaphore(value: 0) // INSERT INTO moves VALUES (1) let s2 = DispatchSemaphore(value: 0) // SELECT SUM(value) AS balance FROM moves let s3 = DispatchSemaphore(value: 0) // INSERT INTO moves VALUES (-1) let s4 = DispatchSemaphore(value: 0) // SELECT SUM(value) AS balance FROM moves // END do { let dbQueue = try makeDatabaseQueue(filename: "test.sqlite") try dbQueue.writeWithoutTransaction { db in let journalMode = try String.fetchOne(db, sql: "PRAGMA journal_mode = wal") XCTAssertEqual(journalMode, "wal") try db.create(table: "moves") { $0.column("value", .integer) } try db.execute(sql: "INSERT INTO moves VALUES (0)") } } let s0 = DispatchSemaphore(value: 0) let block1 = { () in let dbQueue = try! self.makeDatabaseQueue(filename: "test.sqlite") s0.signal() // Avoid "database is locked" error: don't open the two databases at the same time try! dbQueue.writeWithoutTransaction { db in try db.execute(sql: "BEGIN DEFERRED TRANSACTION") s1.signal() _ = s2.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT SUM(value) AS balance FROM moves"), 1) // Non-zero balance s3.signal() _ = s4.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT SUM(value) AS balance FROM moves"), 1) // Non-zero balance try db.execute(sql: "END") } } let block2 = { () in _ = s0.wait(timeout: .distantFuture) // Avoid "database is locked" error: don't open the two databases at the same time let dbQueue = try! self.makeDatabaseQueue(filename: "test.sqlite") try! dbQueue.writeWithoutTransaction { db in _ = s1.wait(timeout: .distantFuture) try db.execute(sql: "INSERT INTO moves VALUES (1)") s2.signal() _ = s3.wait(timeout: .distantFuture) try db.execute(sql: "INSERT INTO moves VALUES (-1)") s4.signal() } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testDatabasePoolFundamental2() throws { // Constraint: the sum of values, the balance, must remain zero. // // This test shows that deferred transactions play well with concurrent // immediate transactions. // // Reader Writer // BEGIN IMMEDIATE TRANSACTION let s1 = DispatchSemaphore(value: 0) // BEGIN DEFERRED TRANSACTION let s2 = DispatchSemaphore(value: 0) // INSERT INTO moves VALUES (1) let s3 = DispatchSemaphore(value: 0) // SELECT SUM(value) AS balance FROM moves let s4 = DispatchSemaphore(value: 0) // INSERT INTO moves VALUES (-1) let s5 = DispatchSemaphore(value: 0) // SELECT SUM(value) AS balance FROM moves // END END do { let dbQueue = try makeDatabaseQueue(filename: "test.sqlite") try dbQueue.writeWithoutTransaction { db in let journalMode = try String.fetchOne(db, sql: "PRAGMA journal_mode = wal") XCTAssertEqual(journalMode, "wal") try db.create(table: "moves") { $0.column("value", .integer) } try db.execute(sql: "INSERT INTO moves VALUES (0)") } } let s0 = DispatchSemaphore(value: 0) let block1 = { () in let dbQueue = try! self.makeDatabaseQueue(filename: "test.sqlite") s0.signal() // Avoid "database is locked" error: don't open the two databases at the same time try! dbQueue.writeWithoutTransaction { db in _ = s1.wait(timeout: .distantFuture) try db.execute(sql: "BEGIN DEFERRED TRANSACTION") s2.signal() _ = s3.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT SUM(value) AS balance FROM moves"), 0) // Zero balance s4.signal() _ = s5.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT SUM(value) AS balance FROM moves"), 0) // Zero balance try db.execute(sql: "END") } } let block2 = { () in _ = s0.wait(timeout: .distantFuture) // Avoid "database is locked" error: don't open the two databases at the same time let dbQueue = try! self.makeDatabaseQueue(filename: "test.sqlite") try! dbQueue.writeWithoutTransaction { db in try db.execute(sql: "BEGIN DEFERRED TRANSACTION") s1.signal() _ = s2.wait(timeout: .distantFuture) try db.execute(sql: "INSERT INTO moves VALUES (1)") s3.signal() _ = s4.wait(timeout: .distantFuture) try db.execute(sql: "INSERT INTO moves VALUES (-1)") s5.signal() try db.execute(sql: "END") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testDatabasePoolFundamental3() throws { // Constraint: the sum of values, the balance, must remain zero. // // This test shows that deferred transactions play well with concurrent // immediate transactions. // // Reader Writer // BEGIN DEFERRED TRANSACTION let s1 = DispatchSemaphore(value: 0) // BEGIN IMMEDIATE TRANSACTION // INSERT INTO moves VALUES (1) let s2 = DispatchSemaphore(value: 0) // SELECT SUM(value) AS balance FROM moves let s3 = DispatchSemaphore(value: 0) // INSERT INTO moves VALUES (-1) let s4 = DispatchSemaphore(value: 0) // SELECT SUM(value) AS balance FROM moves END // END do { let dbQueue = try makeDatabaseQueue(filename: "test.sqlite") try dbQueue.writeWithoutTransaction { db in let journalMode = try String.fetchOne(db, sql: "PRAGMA journal_mode = wal") XCTAssertEqual(journalMode, "wal") try db.create(table: "moves") { $0.column("value", .integer) } try db.execute(sql: "INSERT INTO moves VALUES (0)") } } let s0 = DispatchSemaphore(value: 0) let block1 = { () in let dbQueue = try! self.makeDatabaseQueue(filename: "test.sqlite") s0.signal() // Avoid "database is locked" error: don't open the two databases at the same time try! dbQueue.writeWithoutTransaction { db in try db.execute(sql: "BEGIN DEFERRED TRANSACTION") s1.signal() _ = s2.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT SUM(value) AS balance FROM moves"), 0) // Zero balance s3.signal() _ = s4.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT SUM(value) AS balance FROM moves"), 0) // Zero balance try db.execute(sql: "END") } } let block2 = { () in _ = s0.wait(timeout: .distantFuture) // Avoid "database is locked" error: don't open the two databases at the same time let dbQueue = try! self.makeDatabaseQueue(filename: "test.sqlite") try! dbQueue.writeWithoutTransaction { db in _ = s1.wait(timeout: .distantFuture) try db.execute(sql: "BEGIN DEFERRED TRANSACTION") try db.execute(sql: "INSERT INTO moves VALUES (1)") s2.signal() _ = s3.wait(timeout: .distantFuture) try db.execute(sql: "INSERT INTO moves VALUES (-1)") s4.signal() try db.execute(sql: "END") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testWrappedReadWrite() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") } let id = try dbPool.read { db in try Int.fetchOne(db, sql: "SELECT id FROM items")! } XCTAssertEqual(id, 1) } func testReadFromPreviousNonWALDatabase() throws { do { let dbQueue = try makeDatabaseQueue(filename: "test.sqlite") try dbQueue.writeWithoutTransaction { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") } } do { let dbPool = try makeDatabasePool(filename: "test.sqlite") let id = try dbPool.read { db in try Int.fetchOne(db, sql: "SELECT id FROM items")! } XCTAssertEqual(id, 1) } } func testWriteOpensATransaction() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in XCTAssertTrue(db.isInsideTransaction) do { try db.execute(sql: "BEGIN DEFERRED TRANSACTION") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.message!, "cannot start a transaction within a transaction") XCTAssertEqual(error.sql!, "BEGIN DEFERRED TRANSACTION") XCTAssertEqual(error.description, "SQLite error 1: cannot start a transaction within a transaction - while executing `BEGIN DEFERRED TRANSACTION`") } } } func testWriteWithoutTransactionDoesNotOpenATransaction() throws { let dbPool = try makeDatabasePool() try dbPool.writeWithoutTransaction { db in XCTAssertFalse(db.isInsideTransaction) try db.beginTransaction() try db.commit() } } func testReadOpensATransaction() throws { let dbPool = try makeDatabasePool() try dbPool.read { db in XCTAssertTrue(db.isInsideTransaction) do { try db.execute(sql: "BEGIN DEFERRED TRANSACTION") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.message!, "cannot start a transaction within a transaction") XCTAssertEqual(error.sql!, "BEGIN DEFERRED TRANSACTION") XCTAssertEqual(error.description, "SQLite error 1: cannot start a transaction within a transaction - while executing `BEGIN DEFERRED TRANSACTION`") } } } func testReadError() throws { // Necessary for this test to run as quickly as possible dbConfiguration.readonlyBusyMode = .immediateError let dbPool = try makeDatabasePool() // Block 1 Block 2 // PRAGMA locking_mode=EXCLUSIVE // CREATE TABLE // > let s1 = DispatchSemaphore(value: 0) // dbPool.read // throws let block1 = { () in try! dbPool.writeWithoutTransaction { db in try db.execute(sql: "PRAGMA locking_mode=EXCLUSIVE") try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") s1.signal() } } let block2 = { () in _ = s1.wait(timeout: .distantFuture) do { try dbPool.read { _ in } XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_BUSY) XCTAssertEqual(error.message!, "database is locked") } catch { XCTFail("Expected DatabaseError") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testConcurrentRead() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") for _ in 0..<3 { try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") } } // Block 1 Block 2 // dbPool.read { dbPool.read { // SELECT * FROM items SELECT * FROM items // step step // > let s1 = DispatchSemaphore(value: 0) // step // < let s2 = DispatchSemaphore(value: 0) // step step // step end // end } // } let block1 = { () in try! dbPool.read { db in let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items") XCTAssertTrue(try cursor.next() != nil) s1.signal() _ = s2.wait(timeout: .distantFuture) XCTAssertTrue(try cursor.next() != nil) XCTAssertTrue(try cursor.next() != nil) XCTAssertTrue(try cursor.next() == nil) } } let block2 = { () in try! dbPool.read { db in let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items") XCTAssertTrue(try cursor.next() != nil) _ = s1.wait(timeout: .distantFuture) XCTAssertTrue(try cursor.next() != nil) s2.signal() XCTAssertTrue(try cursor.next() != nil) XCTAssertTrue(try cursor.next() == nil) } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testReadMethodIsolationOfStatement() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") for _ in 0..<2 { try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") } } // Block 1 Block 2 // dbPool.read { // SELECT * FROM items // step // > let s1 = DispatchSemaphore(value: 0) // DELETE * FROM items // < let s2 = DispatchSemaphore(value: 0) // step // end // } let block1 = { () in try! dbPool.read { db in let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items") XCTAssertTrue(try cursor.next() != nil) s1.signal() _ = s2.wait(timeout: .distantFuture) XCTAssertTrue(try cursor.next() != nil) XCTAssertTrue(try cursor.next() == nil) } } let block2 = { () in do { _ = s1.wait(timeout: .distantFuture) defer { s2.signal() } try dbPool.writeWithoutTransaction { db in try db.execute(sql: "DELETE FROM items") } } catch { XCTFail("error: \(error)") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testReadMethodIsolationOfStatementWithCheckpoint() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") for _ in 0..<2 { try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") } } // Block 1 Block 2 // dbPool.read { // SELECT * FROM items // step // > let s1 = DispatchSemaphore(value: 0) // DELETE * FROM items // Checkpoint // < let s2 = DispatchSemaphore(value: 0) // step // end // } let block1 = { () in try! dbPool.read { db in let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items") XCTAssertTrue(try cursor.next() != nil) s1.signal() _ = s2.wait(timeout: .distantFuture) XCTAssertTrue(try cursor.next() != nil) XCTAssertTrue(try cursor.next() == nil) } } let block2 = { () in do { _ = s1.wait(timeout: .distantFuture) defer { s2.signal() } try dbPool.writeWithoutTransaction { db in try db.execute(sql: "DELETE FROM items") try db.checkpoint() } } catch { XCTFail("error: \(error)") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testReadBlockIsolationStartingWithRead() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") } // Block 1 Block 2 // dbPool.read { // > let s1 = DispatchSemaphore(value: 0) // INSERT INTO items (id) VALUES (NULL) // < let s2 = DispatchSemaphore(value: 0) // SELECT COUNT(*) FROM items -> 0 // > let s3 = DispatchSemaphore(value: 0) // INSERT INTO items (id) VALUES (NULL) // < let s4 = DispatchSemaphore(value: 0) // SELECT COUNT(*) FROM items -> 0 // } let block1 = { () in try! dbPool.read { db in s1.signal() _ = s2.wait(timeout: .distantFuture) // We read 0 due to snaphot isolation: the reader has checked // the schema version before `s1` could let the writer insert // an item. XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0) s3.signal() _ = s4.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0) } } let block2 = { () in do { _ = s1.wait(timeout: .distantFuture) try dbPool.writeWithoutTransaction { db in try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") s2.signal() _ = s3.wait(timeout: .distantFuture) try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") s4.signal() } } catch { XCTFail("error: \(error)") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testReadBlockIsolationStartingWithSelect() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") } // Block 1 Block 2 // dbPool.read { // SELECT COUNT(*) FROM items -> 0 // > let s1 = DispatchSemaphore(value: 0) // INSERT INTO items (id) VALUES (NULL) // Checkpoint // < let s2 = DispatchSemaphore(value: 0) // SELECT COUNT(*) FROM items -> 0 // } let block1 = { () in try! dbPool.read { db in XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0) s1.signal() _ = s2.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0) } } let block2 = { () in do { _ = s1.wait(timeout: .distantFuture) defer { s2.signal() } try dbPool.writeWithoutTransaction { db in try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") try db.checkpoint() } } catch { XCTFail("error: \(error)") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testReadBlockIsolationStartingWithWrite() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") } // Block 1 Block 2 // INSERT INTO items (id) VALUES (NULL) // > let s1 = DispatchSemaphore(value: 0) // dbPool.read { // SELECT COUNT(*) FROM items -> 1 // < let s2 = DispatchSemaphore(value: 0) // INSERT INTO items (id) VALUES (NULL) // > let s3 = DispatchSemaphore(value: 0) // SELECT COUNT(*) FROM items -> 1 // < let s4 = DispatchSemaphore(value: 0) // > let s5 = DispatchSemaphore(value: 0) // SELECT COUNT(*) FROM items -> 1 // } let block1 = { () in do { try dbPool.writeWithoutTransaction { db in try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") s1.signal() _ = s2.wait(timeout: .distantFuture) try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") s3.signal() _ = s4.wait(timeout: .distantFuture) } s5.signal() } catch { XCTFail("error: \(error)") } } let block2 = { () in try! dbPool.read { db in _ = s1.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 1) s2.signal() _ = s3.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 1) s4.signal() _ = s5.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 1) } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testReadBlockIsolationStartingWithWriteTransaction() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") } // Block 1 Block 2 // BEGIN IMMEDIATE TRANSACTION // INSERT INTO items (id) VALUES (NULL) // > let s1 = DispatchSemaphore(value: 0) // dbPool.read { // SELECT COUNT(*) FROM items -> 0 // < let s2 = DispatchSemaphore(value: 0) // INSERT INTO items (id) VALUES (NULL) // > let s3 = DispatchSemaphore(value: 0) // SELECT COUNT(*) FROM items -> 0 // < let s4 = DispatchSemaphore(value: 0) // COMMIT // > let s5 = DispatchSemaphore(value: 0) // SELECT COUNT(*) FROM items -> 0 // } let block1 = { () in do { try dbPool.writeInTransaction(.immediate) { db in try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") s1.signal() _ = s2.wait(timeout: .distantFuture) try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") s3.signal() _ = s4.wait(timeout: .distantFuture) return .commit } s5.signal() } catch { XCTFail("error: \(error)") } } let block2 = { () in try! dbPool.read { db in _ = s1.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0) s2.signal() _ = s3.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0) s4.signal() _ = s5.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0) } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testUnsafeReadMethodIsolationOfStatement() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") for _ in 0..<2 { try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") } } // Block 1 Block 2 // dbPool.unsafeRead { // SELECT * FROM items // step // > let s1 = DispatchSemaphore(value: 0) // DELETE * FROM items // < let s2 = DispatchSemaphore(value: 0) // step // end // SELECT COUNT(*) FROM items -> 0 // } let block1 = { () in try! dbPool.unsafeRead { db in let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items") XCTAssertTrue(try cursor.next() != nil) s1.signal() _ = s2.wait(timeout: .distantFuture) XCTAssertTrue(try cursor.next() != nil) XCTAssertTrue(try cursor.next() == nil) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0) } } let block2 = { () in do { _ = s1.wait(timeout: .distantFuture) defer { s2.signal() } try dbPool.writeWithoutTransaction { db in try db.execute(sql: "DELETE FROM items") } } catch { XCTFail("error: \(error)") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testUnsafeReadMethodIsolationOfStatementWithCheckpoint() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") for _ in 0..<2 { try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") } } // Block 1 Block 2 // dbPool.unsafeRead { // SELECT * FROM items // step // > let s1 = DispatchSemaphore(value: 0) // DELETE * FROM items // Checkpoint // < let s2 = DispatchSemaphore(value: 0) // step // end // } let block1 = { () in try! dbPool.unsafeRead { db in let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items") XCTAssertTrue(try cursor.next() != nil) s1.signal() _ = s2.wait(timeout: .distantFuture) XCTAssertTrue(try cursor.next() != nil) XCTAssertTrue(try cursor.next() == nil) } } let block2 = { () in do { _ = s1.wait(timeout: .distantFuture) defer { s2.signal() } try dbPool.writeWithoutTransaction { db in try db.execute(sql: "DELETE FROM items") try db.checkpoint() } } catch { XCTFail("error: \(error)") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testUnsafeReadMethodIsolationOfBlock() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") } // Block 1 Block 2 // dbPool.unsafeRead { // SELECT COUNT(*) FROM items -> 0 // > let s1 = DispatchSemaphore(value: 0) // INSERT INTO items (id) VALUES (NULL) // Checkpoint // < let s2 = DispatchSemaphore(value: 0) // SELECT COUNT(*) FROM items -> 1 // } let block1 = { () in try! dbPool.unsafeRead { db in XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0) s1.signal() _ = s2.wait(timeout: .distantFuture) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 1) } } let block2 = { () in do { _ = s1.wait(timeout: .distantFuture) defer { s2.signal() } try dbPool.writeWithoutTransaction { db in try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") try db.checkpoint() } } catch { XCTFail("error: \(error)") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testLongRunningReadTransaction() throws { // A test for a "user-defined" DatabaseSnapshot based on DatabaseQueue let dbName = "test.sqlite" let dbPool = try makeDatabasePool(filename: dbName) dbConfiguration.allowsUnsafeTransactions = true dbConfiguration.readonly = true let dbQueue = try makeDatabaseQueue(filename: dbName) try dbPool.write { db in try db.create(table: "t") { $0.primaryKey("id", .integer) } try db.execute(sql: "INSERT INTO t DEFAULT VALUES") } try dbQueue.writeWithoutTransaction { db in try db.beginTransaction(.deferred) } try dbQueue.writeWithoutTransaction { db in try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 1) } try dbPool.write { db in try db.execute(sql: "INSERT INTO t DEFAULT VALUES") } try dbQueue.writeWithoutTransaction { db in try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 1) try db.rollback() try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 2) } } func testIssue80() throws { // See https://github.com/groue/GRDB.swift/issues/80 // // Here we test that `SELECT * FROM search`, which is announced by // SQLite has a statement that modifies the sqlite_master table, is // still allowed as a regular select statement. // // This test uses a database pool, a connection for creating the // search virtual table, and another connection for reading. // // This is the tested setup: don't change it. let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE VIRTUAL TABLE search USING fts3(title)") } try dbPool.read { db in _ = try Row.fetchAll(db, sql: "SELECT * FROM search") } } func testDefaultLabel() throws { let dbPool = try makeDatabasePool() dbPool.writeWithoutTransaction { db in XCTAssertEqual(db.configuration.label, nil) XCTAssertEqual(db.description, "GRDB.DatabasePool.writer") // This test CAN break in future releases: the dispatch queue labels // are documented to be a debug-only tool. let label = String(utf8String: __dispatch_queue_get_label(nil)) XCTAssertEqual(label, "GRDB.DatabasePool.writer") } let s1 = DispatchSemaphore(value: 0) let s2 = DispatchSemaphore(value: 0) let block1 = { () in try! dbPool.read { db in XCTAssertEqual(db.configuration.label, nil) XCTAssertEqual(db.description, "GRDB.DatabasePool.reader.1") // This test CAN break in future releases: the dispatch queue labels // are documented to be a debug-only tool. let label = String(utf8String: __dispatch_queue_get_label(nil)) XCTAssertEqual(label, "GRDB.DatabasePool.reader.1") _ = s1.signal() _ = s2.wait(timeout: .distantFuture) } } let block2 = { () in _ = s1.wait(timeout: .distantFuture) try! dbPool.read { db in _ = s2.signal() XCTAssertEqual(db.configuration.label, nil) XCTAssertEqual(db.description, "GRDB.DatabasePool.reader.2") // This test CAN break in future releases: the dispatch queue labels // are documented to be a debug-only tool. let label = String(utf8String: __dispatch_queue_get_label(nil)) XCTAssertEqual(label, "GRDB.DatabasePool.reader.2") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testCustomLabel() throws { dbConfiguration.label = "Toreador" let dbPool = try makeDatabasePool() dbPool.writeWithoutTransaction { db in XCTAssertEqual(db.configuration.label, "Toreador") XCTAssertEqual(db.description, "Toreador.writer") // This test CAN break in future releases: the dispatch queue labels // are documented to be a debug-only tool. let label = String(utf8String: __dispatch_queue_get_label(nil)) XCTAssertEqual(label, "Toreador.writer") } let s1 = DispatchSemaphore(value: 0) let s2 = DispatchSemaphore(value: 0) let block1 = { () in try! dbPool.read { db in XCTAssertEqual(db.configuration.label, "Toreador") XCTAssertEqual(db.description, "Toreador.reader.1") // This test CAN break in future releases: the dispatch queue labels // are documented to be a debug-only tool. let label = String(utf8String: __dispatch_queue_get_label(nil)) XCTAssertEqual(label, "Toreador.reader.1") _ = s1.signal() _ = s2.wait(timeout: .distantFuture) } } let block2 = { () in _ = s1.wait(timeout: .distantFuture) try! dbPool.read { db in _ = s2.signal() XCTAssertEqual(db.configuration.label, "Toreador") XCTAssertEqual(db.description, "Toreador.reader.2") // This test CAN break in future releases: the dispatch queue labels // are documented to be a debug-only tool. let label = String(utf8String: __dispatch_queue_get_label(nil)) XCTAssertEqual(label, "Toreador.reader.2") } } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } func testTargetQueue() throws { func test(targetQueue: DispatchQueue) throws { dbConfiguration.targetQueue = targetQueue let dbPool = try makeDatabasePool() try dbPool.write { _ in dispatchPrecondition(condition: .onQueue(targetQueue)) } try dbPool.read { _ in dispatchPrecondition(condition: .onQueue(targetQueue)) } } // background queue try test(targetQueue: .global(qos: .background)) // main queue let expectation = self.expectation(description: "main") DispatchQueue.global(qos: .default).async { try! test(targetQueue: .main) expectation.fulfill() } waitForExpectations(timeout: 2, handler: nil) } func testWriteTargetQueue() throws { func test(targetQueue: DispatchQueue, writeTargetQueue: DispatchQueue) throws { dbConfiguration.targetQueue = targetQueue dbConfiguration.writeTargetQueue = writeTargetQueue let dbPool = try makeDatabasePool() try dbPool.write { _ in dispatchPrecondition(condition: .onQueue(writeTargetQueue)) } try dbPool.read { _ in dispatchPrecondition(condition: .onQueue(targetQueue)) } } // background queue try test(targetQueue: .global(qos: .background), writeTargetQueue: DispatchQueue(label: "writer")) // main queue let expectation = self.expectation(description: "main") DispatchQueue.global(qos: .default).async { try! test(targetQueue: .main, writeTargetQueue: .main) expectation.fulfill() } waitForExpectations(timeout: 2, handler: nil) } func testWriteTargetQueueReadOnly() throws { // Create a database before we perform read-only accesses _ = try makeDatabasePool(filename: "test") func test(targetQueue: DispatchQueue, writeTargetQueue: DispatchQueue) throws { dbConfiguration.readonly = true dbConfiguration.targetQueue = targetQueue dbConfiguration.writeTargetQueue = writeTargetQueue // unused let dbPool = try makeDatabasePool(filename: "test") try dbPool.write { _ in // Not on writeTargetQueue because read-only dispatchPrecondition(condition: .onQueue(targetQueue)) } try dbPool.read { _ in dispatchPrecondition(condition: .onQueue(targetQueue)) } } try test(targetQueue: .global(qos: .background), writeTargetQueue: DispatchQueue(label: "writer")) } func testQoS() throws { func test(qos: DispatchQoS) throws { // https://forums.swift.org/t/what-is-the-default-target-queue-for-a-serial-queue/18094/5 // // > [...] the default target queue [for a serial queue] is the // > [default] overcommit [global concurrent] queue. // // We want this default target queue in order to test database QoS // with dispatchPrecondition(condition:). // // > [...] You can get a reference to the overcommit queue by // > dropping down to the C function dispatch_get_global_queue // > (available in Swift with a __ prefix) and passing the private // > value of DISPATCH_QUEUE_OVERCOMMIT. // > // > [...] Of course you should not do this in production code, // > because DISPATCH_QUEUE_OVERCOMMIT is not a public API. I don't // > know of a way to get a reference to the overcommit queue using // > only public APIs. let DISPATCH_QUEUE_OVERCOMMIT: UInt = 2 let targetQueue = __dispatch_get_global_queue( Int(qos.qosClass.rawValue.rawValue), DISPATCH_QUEUE_OVERCOMMIT) dbConfiguration.qos = qos let dbPool = try makeDatabasePool() try dbPool.write { _ in dispatchPrecondition(condition: .onQueue(targetQueue)) } try dbPool.read { _ in dispatchPrecondition(condition: .onQueue(targetQueue)) } } try test(qos: .background) try test(qos: .userInitiated) } // MARK: - ConcurrentRead func testConcurrentReadOpensATransaction() throws { let dbPool = try makeDatabasePool() let future = dbPool.writeWithoutTransaction { db in dbPool.concurrentRead { db in XCTAssertTrue(db.isInsideTransaction) do { try db.execute(sql: "BEGIN DEFERRED TRANSACTION") XCTFail("Expected error") } catch { } } } try future.wait() } func testConcurrentReadOutsideOfTransaction() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.create(table: "persons") { t in t.primaryKey("id", .integer) } } // Writer Reader // dbPool.writeWithoutTransaction { // > // dbPool.concurrentRead { // < // INSERT INTO items (id) VALUES (NULL) // > let s1 = DispatchSemaphore(value: 0) // } SELECT COUNT(*) FROM persons -> 0 // < // } let future: DatabaseFuture = try dbPool.writeWithoutTransaction { db in let future: DatabaseFuture = dbPool.concurrentRead { db in _ = s1.wait(timeout: .distantFuture) return try! Int.fetchOne(db, sql: "SELECT COUNT(*) FROM persons")! } try db.execute(sql: "INSERT INTO persons DEFAULT VALUES") s1.signal() return future } XCTAssertEqual(try future.wait(), 0) } func testConcurrentReadError() throws { // Necessary for this test to run as quickly as possible dbConfiguration.readonlyBusyMode = .immediateError let dbPool = try makeDatabasePool() try dbPool.writeWithoutTransaction { db in try db.execute(sql: "PRAGMA locking_mode=EXCLUSIVE") try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") let future = dbPool.concurrentRead { db in fatalError("Should not run") } do { try future.wait() } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_BUSY) XCTAssertEqual(error.message!, "database is locked") } } } // MARK: - AsyncConcurrentRead func testAsyncConcurrentReadOpensATransaction() throws { let dbPool = try makeDatabasePool() var isInsideTransaction: Bool? = nil let expectation = self.expectation(description: "read") dbPool.writeWithoutTransaction { db in dbPool.asyncConcurrentRead { dbResult in do { let db = try dbResult.get() isInsideTransaction = db.isInsideTransaction do { try db.execute(sql: "BEGIN DEFERRED TRANSACTION") XCTFail("Expected error") } catch { } } catch { XCTFail("Unexpected error: \(error)") } expectation.fulfill() } } waitForExpectations(timeout: 1, handler: nil) XCTAssertEqual(isInsideTransaction, true) } func testAsyncConcurrentReadOutsideOfTransaction() throws { let dbPool = try makeDatabasePool() try dbPool.write { db in try db.create(table: "persons") { t in t.primaryKey("id", .integer) } } // Writer Reader // dbPool.writeWithoutTransaction { // > // dbPool.concurrentRead { // < // INSERT INTO items (id) VALUES (NULL) // > let s1 = DispatchSemaphore(value: 0) // } SELECT COUNT(*) FROM persons -> 0 // < // } var count: Int? = nil let expectation = self.expectation(description: "read") try dbPool.writeWithoutTransaction { db in dbPool.asyncConcurrentRead { dbResult in do { _ = s1.wait(timeout: .distantFuture) let db = try dbResult.get() count = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM persons")! } catch { XCTFail("Unexpected error: \(error)") } expectation.fulfill() } try db.execute(sql: "INSERT INTO persons DEFAULT VALUES") s1.signal() } waitForExpectations(timeout: 1, handler: nil) XCTAssertEqual(count, 0) } func testAsyncConcurrentReadError() throws { // Necessary for this test to run as quickly as possible dbConfiguration.readonlyBusyMode = .immediateError let dbPool = try makeDatabasePool() var readError: DatabaseError? = nil let expectation = self.expectation(description: "read") try dbPool.writeWithoutTransaction { db in try db.execute(sql: "PRAGMA locking_mode=EXCLUSIVE") try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") dbPool.asyncConcurrentRead { dbResult in guard case let .failure(error) = dbResult, let dbError = error as? DatabaseError else { XCTFail("Unexpected result: \(dbResult)") return } readError = dbError expectation.fulfill() } waitForExpectations(timeout: 1, handler: nil) XCTAssertEqual(readError!.resultCode, .SQLITE_BUSY) XCTAssertEqual(readError!.message!, "database is locked") } } // MARK: - Barrier func testBarrierLocksReads() throws { let expectation = self.expectation(description: "lock") expectation.isInverted = true let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") } var fetchedValue: Int? let s1 = DispatchSemaphore(value: 0) let s2 = DispatchSemaphore(value: 0) let s3 = DispatchSemaphore(value: 0) DispatchQueue.global().async { try! dbPool.barrierWriteWithoutTransaction { db in try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)") s1.signal() s2.wait() } } DispatchQueue.global().async { // Wait for barrier to start s1.wait() fetchedValue = try! dbPool.read { db in try Int.fetchOne(db, sql: "SELECT id FROM items")! } expectation.fulfill() s3.signal() } // Assert that read is blocked waitForExpectations(timeout: 1) // Release barrier s2.signal() // Wait for read to complete s3.wait() XCTAssertEqual(fetchedValue, 1) } func testBarrierIsLockedByOneUnfinishedRead() throws { let expectation = self.expectation(description: "lock") expectation.isInverted = true let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") } let s1 = DispatchSemaphore(value: 0) let s2 = DispatchSemaphore(value: 0) let s3 = DispatchSemaphore(value: 0) let s4 = DispatchSemaphore(value: 0) DispatchQueue.global().async { try! dbPool.read { _ in s1.signal() s2.signal() s3.wait() } } DispatchQueue.global().async { // Wait for read to start s1.wait() try! dbPool.barrierWriteWithoutTransaction { _ in } expectation.fulfill() s4.signal() } // Assert that barrier is blocked waitForExpectations(timeout: 1) // Release read s3.signal() // Wait for barrier to complete s4.wait() } // MARK: - Concurrent opening func testConcurrentOpening() throws { for _ in 0..<50 { let dbDirectoryName = "DatabasePoolConcurrencyTests-\(ProcessInfo.processInfo.globallyUniqueString)" let directoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent(dbDirectoryName, isDirectory: true) let dbURL = directoryURL.appendingPathComponent("db.sqlite") try FileManager.default.createDirectory(atPath: directoryURL.path, withIntermediateDirectories: true, attributes: nil) defer { try! FileManager.default.removeItem(at: directoryURL) } DispatchQueue.concurrentPerform(iterations: 10) { n in // WTF I could never Google for the proper correct error handling // of NSFileCoordinator. What a weird API. let coordinator = NSFileCoordinator(filePresenter: nil) var coordinatorError: NSError? var poolError: Error? coordinator.coordinate(writingItemAt: dbURL, options: .forMerging, error: &coordinatorError) { url in do { _ = try DatabasePool(path: url.path) } catch { poolError = error } } XCTAssert(poolError ?? coordinatorError == nil) } } } // MARK: - NSFileCoordinator sample code tests // Test for sample code in Documentation.docc/DatabaseSharing.md. // This test passes if this method compiles private func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool { let coordinator = NSFileCoordinator(filePresenter: nil) var coordinatorError: NSError? var dbPool: DatabasePool? var dbError: Error? coordinator.coordinate(writingItemAt: databaseURL, options: .forMerging, error: &coordinatorError) { url in do { dbPool = try openDatabase(at: url) } catch { dbError = error } } if let error = dbError ?? coordinatorError { throw error } return dbPool! } // Test for sample code in Documentation.docc/DatabaseSharing.md. // This test passes if this method compiles private func openDatabase(at databaseURL: URL) throws -> DatabasePool { let dbPool = try DatabasePool(path: databaseURL.path) // Perform here other database setups, such as defining // the database schema with a DatabaseMigrator. return dbPool } // Test for sample code in Documentation.docc/DatabaseSharing.md. // This test passes if this method compiles private func openSharedReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? { let coordinator = NSFileCoordinator(filePresenter: nil) var coordinatorError: NSError? var dbPool: DatabasePool? var dbError: Error? coordinator.coordinate(readingItemAt: databaseURL, options: .withoutChanges, error: &coordinatorError) { url in do { dbPool = try openReadOnlyDatabase(at: url) } catch { dbError = error } } if let error = dbError ?? coordinatorError { throw error } return dbPool } // Test for sample code in Documentation.docc/DatabaseSharing.md. // This test passes if this method compiles private func openReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? { do { var configuration = Configuration() configuration.readonly = true return try DatabasePool(path: databaseURL.path, configuration: configuration) } catch { if FileManager.default.fileExists(atPath: databaseURL.path) { // Something went wrong throw error } else { // Database file does not exist return nil } } } }