Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift
T
2025-01-28 12:28:03 +08:00

1419 lines
56 KiB
Swift

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<Int> = try dbPool.writeWithoutTransaction { db in
let future: DatabaseFuture<Int> = 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
}
}
}
}