Skip to content

Commit

Permalink
Merge pull request #1421 from groue/dev/journalModeSetup
Browse files Browse the repository at this point in the history
Make it possible to open a DatabaseQueue in the WAL mode
  • Loading branch information
groue authored Aug 26, 2023
2 parents 97c04a7 + 9e5cb95 commit 6fec245
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 35 deletions.
40 changes: 40 additions & 0 deletions GRDB/Core/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,46 @@ public struct Configuration {
/// of a read access.
public var allowsUnsafeTransactions = false

// MARK: - Journal Mode

/// Defines how the journal mode is configured when the database
/// connection is opened.
///
/// Related SQLite documentation: <https://www.sqlite.org/pragma.html#pragma_journal_mode>
public enum JournalModeConfiguration {
/// The default setup has ``DatabaseQueue`` perform no specific
/// configuration of the journal mode, and ``DatabasePool``
/// configure the database for the WAL mode (just like the
/// ``wal`` case).
case `default`

/// The journal mode is set to WAL (plus extra configurations that
/// make life easier with WAL databases).
case wal
}

/// Defines how the journal mode is configured when the database
/// connection is opened.
///
/// This configuration is ignored when ``readonly`` is true.
///
/// The default value has ``DatabaseQueue`` perform no specific
/// configuration of the journal mode, and ``DatabasePool`` configure
/// the database for the WAL mode.
///
/// Applications that need to open a WAL database with a
/// ``DatabaseQueue`` should set the `journalMode` to `wal`:
///
/// ```swift
/// // Open a WAL database with DatabaseQueue
/// var config = Configuration()
/// config.journalMode = .wal
/// let dbQueue = try DatabaseQueue(path: "...", configuration: config)
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/pragma.html#pragma_journal_mode>
public var journalMode = JournalModeConfiguration.default

// MARK: - Concurrency

/// Defines the how `SQLITE_BUSY` errors are handled.
Expand Down
35 changes: 35 additions & 0 deletions GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,41 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
configuration.SQLiteConnectionDidOpen?()
}

/// Performs ``Configuration/JournalModeConfiguration/wal``.
func setUpWALMode() throws {
let journalMode = try String.fetchOne(self, sql: "PRAGMA journal_mode = WAL")
guard journalMode == "wal" else {
throw DatabaseError(message: "could not activate WAL Mode at path: \(path)")
}

// https://www.sqlite.org/pragma.html#pragma_synchronous
// > Many applications choose NORMAL when in WAL mode
try execute(sql: "PRAGMA synchronous = NORMAL")

// Make sure a non-empty wal file exists.
//
// The presence of the wal file avoids an SQLITE_CANTOPEN (14)
// error when the user opens a pool and reads from it.
// See <https://github.com/groue/GRDB.swift/issues/102>.
//
// The non-empty wal file avoids an SQLITE_ERROR (1) error
// when the user opens a pool and creates a wal snapshot
// (which happens when starting a ValueObservation).
// See <https://github.com/groue/GRDB.swift/issues/1383>.
let walPath = path + "-wal"
if try FileManager.default.fileExists(atPath: walPath) == false
|| (URL(fileURLWithPath: walPath).resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0) == 0
{
try inSavepoint {
try execute(sql: """
CREATE TABLE grdb_issue_102 (id INTEGER PRIMARY KEY);
DROP TABLE grdb_issue_102;
""")
return .commit
}
}
}

private func setupDoubleQuotedStringLiterals() {
if configuration.acceptsDoubleQuotedStringLiterals {
_enableDoubleQuotedStringLiterals(sqliteConnection)
Expand Down
37 changes: 5 additions & 32 deletions GRDB/Core/DatabasePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,39 +75,12 @@ public final class DatabasePool {
purpose: "reader.\(readerCount)")
})

// Activate WAL Mode unless readonly
// Set up journal mode unless readonly
if !configuration.readonly {
try writer.sync { db in
let journalMode = try String.fetchOne(db, sql: "PRAGMA journal_mode = WAL")
guard journalMode == "wal" else {
throw DatabaseError(message: "could not activate WAL Mode at path: \(path)")
}

// https://www.sqlite.org/pragma.html#pragma_synchronous
// > Many applications choose NORMAL when in WAL mode
try db.execute(sql: "PRAGMA synchronous = NORMAL")

// Make sure a non-empty wal file exists.
//
// The presence of the wal file avoids an SQLITE_CANTOPEN (14)
// error when the user opens a pool and reads from it.
// See <https://github.com/groue/GRDB.swift/issues/102>.
//
// The non-empty wal file avoids an SQLITE_ERROR (1) error
// when the user opens a pool and creates a wal snapshot
// (which happens when starting a ValueObservation).
// See <https://github.com/groue/GRDB.swift/issues/1383>.
let walPath = path + "-wal"
if try FileManager.default.fileExists(atPath: walPath) == false
|| (URL(fileURLWithPath: walPath).resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0) == 0
{
try db.inSavepoint {
try db.execute(sql: """
CREATE TABLE grdb_issue_102 (id INTEGER PRIMARY KEY);
DROP TABLE grdb_issue_102;
""")
return .commit
}
switch configuration.journalMode {
case .default, .wal:
try writer.sync {
try $0.setUpWALMode()
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions GRDB/Core/DatabaseQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ public final class DatabaseQueue {
configuration: configuration,
defaultLabel: "GRDB.DatabaseQueue")

// Set up journal mode unless readonly
if !configuration.readonly {
switch configuration.journalMode {
case .default:
break
case .wal:
try writer.sync {
try $0.setUpWALMode()
}
}
}

setupSuspension()

// Be a nice iOS citizen, and don't consume too much memory
Expand Down
8 changes: 5 additions & 3 deletions GRDB/Documentation.docc/DatabaseSharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ We'll address all of those challenges below.
>
> Always consider sharing plain files, or any other inter-process communication technique, before sharing an SQLite database.
## Use a Database Pool
## Use the WAL mode

In order to access a shared database, use a ``DatabasePool``. It opens the database in the [WAL mode], which helps sharing a database.
In order to access a shared database, use a ``DatabasePool``. It opens the database in the [WAL mode], which helps sharing a database because it allows multiple processes to access the database concurrently.

Since several processes may open the database at the same time, protect the creation of the database pool with an [NSFileCoordinator].
It is also possible to use a ``DatabaseQueue``, with the `.wal` ``Configuration/journalMode``.

Since several processes may open the database at the same time, protect the creation of the database connection with an [NSFileCoordinator].

- In a process that can create and write in the database, use this sample code:

Expand Down
2 changes: 2 additions & 0 deletions GRDB/Documentation.docc/Extension/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ do {
- ``acceptsDoubleQuotedStringLiterals``
- ``busyMode``
- ``foreignKeysEnabled``
- ``journalMode``
- ``readonly``
- ``JournalModeConfiguration``

### Configuring GRDB Connections

Expand Down
32 changes: 32 additions & 0 deletions Tests/GRDBTests/DatabasePoolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@ import XCTest
import GRDB

class DatabasePoolTests: GRDBTestCase {
func testJournalModeConfiguration() throws {
do {
// Factory default
let config = Configuration()
let dbPool = try makeDatabasePool(filename: "factory", configuration: config)
let journalMode = try dbPool.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "wal")
}
do {
// Explicit default
var config = Configuration()
config.journalMode = .default
let dbPool = try makeDatabasePool(filename: "default", configuration: config)
let journalMode = try dbPool.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "wal")
}
do {
// Explicit wal
var config = Configuration()
config.journalMode = .wal
let dbPool = try makeDatabasePool(filename: "wal", configuration: config)
let journalMode = try dbPool.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "wal")
}
}

func testDatabasePoolCreatesWalShm() throws {
let dbPool = try makeDatabasePool(filename: "test")
try withExtendedLifetime(dbPool) {
Expand Down
31 changes: 31 additions & 0 deletions Tests/GRDBTests/DatabaseQueueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,37 @@ import Dispatch
import GRDB

class DatabaseQueueTests: GRDBTestCase {
func testJournalModeConfiguration() throws {
do {
// Factory default
let config = Configuration()
let dbQueue = try makeDatabaseQueue(filename: "factory", configuration: config)
let journalMode = try dbQueue.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "delete")
}
do {
// Explicit default
var config = Configuration()
config.journalMode = .default
let dbQueue = try makeDatabaseQueue(filename: "default", configuration: config)
let journalMode = try dbQueue.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "delete")
}
do {
// Explicit wal
var config = Configuration()
config.journalMode = .wal
let dbQueue = try makeDatabaseQueue(filename: "wal", configuration: config)
let journalMode = try dbQueue.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "wal")
}
}

func testInvalidFileFormat() throws {
do {
Expand Down

0 comments on commit 6fec245

Please sign in to comment.