diff --git a/Sources/Lighter/Transactions/SQLDatabaseTransaction.swift b/Sources/Lighter/Transactions/SQLDatabaseTransaction.swift index f03aca9..2885e36 100644 --- a/Sources/Lighter/Transactions/SQLDatabaseTransaction.swift +++ b/Sources/Lighter/Transactions/SQLDatabaseTransaction.swift @@ -1,6 +1,6 @@ // // Created by Helge Heß. -// Copyright © 2022 ZeeZide GmbH. +// Copyright © 2022-2024 ZeeZide GmbH. // import SQLite3 @@ -19,17 +19,23 @@ public extension SQLDatabase { * try tx.update(person) * } * ``` - * If the transaction is read-only (just runs a few selects), - * the optimized ``SQLDatabase/readTransaction(execute:)-5rhrj`` can be used. + * If the transaction is read-only (does no modifications to the database), + * the ``SQLDatabase/readTransaction(execute:)-8mbsj`` should be used. + * SQLite only supports one writer per database, using this method will + * acquire such a lock by default. Using `readTransaction` avoids that + * (multiple readers are allowed, in particular if the DB is set to the WAL + * mode). * * Note: Within a transaction async calls are not allowed (as they can * block the transaction, and with it the database, for a unforseeable * time). * * - Parameters: - * - mode: Can be used to acquire a write lock right away. Defaults to - * ``SQLTransactionType/deferred``, which keeps the tx in read - * mode until the first change operation is issued. + * - mode: The mode defaults to ``SQLTransactionType/immediate``, which + * opens/waits for the database lock right away. + * It can be set to ``SQLTransactionType/deferred`` to start with + * a read-lock, but note that upgrades on locked databases will + * fail w/ `SQLITE_BUSY` immediately. * - execute: The code which is executed within the transaction * - Returns: The result of the `execute` closure if the transaction got * committed successfully. diff --git a/Sources/Lighter/Transactions/SQLTransaction.swift b/Sources/Lighter/Transactions/SQLTransaction.swift index 5a89942..de82372 100644 --- a/Sources/Lighter/Transactions/SQLTransaction.swift +++ b/Sources/Lighter/Transactions/SQLTransaction.swift @@ -1,6 +1,6 @@ // // Created by Helge Heß. -// Copyright © 2022 ZeeZide GmbH. +// Copyright © 2022-2024 ZeeZide GmbH. // /** @@ -28,6 +28,10 @@ * ``` * * Note: Within a transaction async calls are not allowed. + * This is intentional. A transaction can lock database objects, + * often the whole database. An async call can take an unspecified amount + * of time and there are no guarantees when it completes as it is + * cooperatively scheduled. This should leave a DB tx hanging. * * #### Performance * diff --git a/Sources/Lighter/Transactions/SQLTransactionAsync.swift b/Sources/Lighter/Transactions/SQLTransactionAsync.swift index 2f9475a..41e6973 100644 --- a/Sources/Lighter/Transactions/SQLTransactionAsync.swift +++ b/Sources/Lighter/Transactions/SQLTransactionAsync.swift @@ -1,6 +1,6 @@ // // Created by Helge Heß. -// Copyright © 2022 ZeeZide GmbH. +// Copyright © 2022-2024 ZeeZide GmbH. // // The async/await variants of the SQLDatabase transaction operations, @@ -23,17 +23,23 @@ public extension SQLDatabase where Self: SQLDatabaseAsyncOperations { * try tx.update(person) * } * ``` - * If the transaction is read-only (just runs a few selects), - * the optimized ``SQLDatabase/readTransaction(execute:)-8mbsj`` can be used. + * If the transaction is read-only (does no modifications to the database), + * the ``SQLDatabase/readTransaction(execute:)-8mbsj`` should be used. + * SQLite only supports one writer per database, using this method will + * acquire such a lock by default. Using `readTransaction` avoids that + * (multiple readers are allowed, in particular if the DB is set to the WAL + * mode). * * Note: Within a transaction async calls are not allowed (as they can * block the transaction, and with it the database, for a unforseeable * time). * * - Parameters: - * - mode: Can be used to acquire a write lock right away. Defaults to - * ``SQLTransactionType/deferred``, which keeps the tx in read - * mode until the first change operation is issued. + * - mode: The mode defaults to ``SQLTransactionType/immediate``, which + * opens/waits for the database lock right away. + * It can be set to ``SQLTransactionType/deferred`` to start with + * a read-lock, but note that upgrades on locked databases will + * fail w/ `SQLITE_BUSY` immediately. * - execute: The code which is executed within the transaction * - Returns: The result of the `execute` closure if the transaction got * committed successfully. diff --git a/Sources/Lighter/Transactions/SQLTransactionType.swift b/Sources/Lighter/Transactions/SQLTransactionType.swift index e293821..8402b54 100644 --- a/Sources/Lighter/Transactions/SQLTransactionType.swift +++ b/Sources/Lighter/Transactions/SQLTransactionType.swift @@ -1,6 +1,6 @@ // // Created by Helge Heß. -// Copyright © 2022 ZeeZide GmbH. +// Copyright © 2022-2024 ZeeZide GmbH. // /** @@ -8,20 +8,28 @@ * and in non-WAL mode, whether a transaction is exclusive (i.e. forbids * concurrent reads). * - * The default is ``deferred``, which keeps the transaction in read mode until - * the first modifying operation is issued (e.g. a delete or insert). + * The default is ``immediate``, which directly acquires the database write + * lock. + * It is preferred over ``deferred``, because transaction upgrades will + * immediately fail w/ `SQLITE_BUSY` if the database lock is in use. + * While an immediate transaction will wait to acquire the lock. */ public enum SQLTransactionType: String { /// Start a read transaction on the first SELECT and upgrade to a write /// transaction on the first modification. + /// Careful: When transactions are upgraded by writes and the database is + /// locked already, a `SQLITE_BUSY` error will be issued immediately + /// (i.e. it won't wait for the lock becoming available). case deferred = "DEFERRED" - - /// Immediatly start a writable transaction + + /// Immediatly start a writable transaction. This will acquire (and possibly + /// wait) for the database write lock. case immediate = "IMMEDIATE" - /// The same like ``immediate`` in WAL mode, but forbids reads in others. + /// The same like ``immediate`` in WAL mode, but protects against concurrent + /// reads in others. case exclusive = "EXCLUSIVE" - public static let `default` = SQLTransactionType.deferred + public static let `default` = SQLTransactionType.immediate }