Skip to content

Commit 4563f1c

Browse files
authored
Use writeWithoutTransaction when appropriate (#53)
This pull request fixes SQLITE_BUSY errors caused by the shift to IMMEDIATE transactions. Updating the transaction type https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions from the default DEFERRED to IMMEDIATE exposed some issues with the way SQLite wrapped GRDB. SQLite was wrapping some writes in an unnecessary transaction. This caused issues because IMMEDIATE transactions immediately begin a write, meaning other writes will receive a SQLITE_BUSY error. Before applying the changes in this pull request, vacuuming and checkpointing tests failed when IMMEDIATE transactions were used.
1 parent ed19230 commit 4563f1c

File tree

3 files changed

+42
-9
lines changed

3 files changed

+42
-9
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ on: push
55
jobs:
66
test:
77
runs-on: macos-13
8-
8+
99
steps:
1010
- uses: actions/checkout@v3
1111
- name: Select Xcode 15
1212
run: sudo xcode-select -s /Applications/Xcode_15.0.app
1313
- name: Test
1414
run: swift test
15+

Sources/SQLite/SQLiteDatabase.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ public final class SQLiteDatabase: DatabaseProtocol, @unchecked Sendable {
111111
)
112112
}
113113

114+
public func truncate() throws {
115+
switch database {
116+
case let .pool(pool):
117+
try pool.writeWithoutTransaction { db in
118+
_ = try db.execute(raw: "PRAGMA wal_checkpoint(TRUNCATE);")
119+
}
120+
121+
case let .queue(queue):
122+
try queue.writeWithoutTransaction { db in
123+
_ = try db.execute(raw: "PRAGMA wal_checkpoint(TRUNCATE);")
124+
}
125+
}
126+
}
127+
114128
// NOTE: This function is only really meant to be called in tests.
115129
public func close() throws {
116130
changeNotifier.stop()
@@ -275,7 +289,7 @@ public extension SQLiteDatabase {
275289
@discardableResult
276290
func execute(raw sql: SQL) async throws -> [SQLiteRow] {
277291
do {
278-
return try await database.writer.write { db in
292+
return try await database.writer.writeWithoutTransaction { db in
279293
try db.execute(raw: sql)
280294
}
281295
} catch {
@@ -339,7 +353,7 @@ public extension SQLiteDatabase {
339353
@discardableResult
340354
func execute(raw sql: SQL) throws -> [SQLiteRow] {
341355
do {
342-
return try database.writer.write { db in
356+
return try database.writer.writeWithoutTransaction { db in
343357
try db.execute(raw: sql)
344358
}
345359
} catch {
@@ -644,13 +658,10 @@ private extension SQLiteDatabase {
644658
config.journalMode = isInMemory ? .default : .wal
645659
// NOTE: GRDB recommends `defaultTransactionKind` be set
646660
// to `.immediate` in order to prevent `SQLITE_BUSY`
647-
// errors. Using `.immediate` appears to disable
648-
// automatic vacuuming.
661+
// errors.
649662
//
650663
// https://swiftpackageindex.com/groue/grdb.swift/v6.24.2/documentation/grdb/databasesharing#How-to-limit-the-SQLITEBUSY-error
651-
config.defaultTransactionKind = isInMemory
652-
? .deferred
653-
: .immediate
664+
config.defaultTransactionKind = .immediate
654665
config.busyMode = .timeout(busyTimeout)
655666
config.observesSuspensionNotifications = true
656667
config.maximumReaderCount = max(
@@ -770,7 +781,7 @@ private enum Database {
770781
arguments: SQLiteArguments = [:]
771782
) throws {
772783
do {
773-
try writer.write { db in
784+
try writer.writeWithoutTransaction { db in
774785
try db.write(sql, arguments: arguments)
775786
}
776787
} catch {

Tests/SQLiteTests/SQLiteDatabaseTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,27 @@ final class SQLiteDatabaseTests: XCTestCase {
253253
}
254254
}
255255

256+
func testCheckpointUsingTruncate() throws {
257+
try Sandbox.execute { directory in
258+
let path = directory.appendingPathComponent("test.db").path
259+
let db = try SQLiteDatabase(path: path)
260+
defer { try? db.close() }
261+
262+
try db.execute(raw: _createTableWithBlob)
263+
264+
try db.inTransaction { db in
265+
for index in 0 ..< 100 {
266+
let args: SQLiteArguments = [
267+
"id": .integer(Int64(index)), "data": .data(_textData),
268+
]
269+
try db.write(_insertIDAndData, arguments: args)
270+
}
271+
}
272+
273+
try db.truncate()
274+
}
275+
}
276+
256277
func testCreateTable() throws {
257278
XCTAssertNoThrow(try database.execute(raw: _createTableWithBlob))
258279
let tableNames = try database.tables()

0 commit comments

Comments
 (0)