Skip to content

Commit

Permalink
Transaction Date
Browse files Browse the repository at this point in the history
  • Loading branch information
groue committed Feb 18, 2023
1 parent 3135067 commit f649d9e
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 0 deletions.
4 changes: 4 additions & 0 deletions GRDB.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@
56AACAA822ACED7100A40F2A /* Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AACAA722ACED7100A40F2A /* Fetch.swift */; };
56AE64122229A53700AD1B0B /* HasOneThroughAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE64112229A53700AD1B0B /* HasOneThroughAssociation.swift */; };
56AE6424222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE6423222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift */; };
56AFEF2F29969F6E00CA1E51 /* TransactionClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AFEF2E29969F6E00CA1E51 /* TransactionClock.swift */; };
56B021C91D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B021C81D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */; };
56B6EF56208CB4E3002F0ACB /* ColumnExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B6EF55208CB4E3002F0ACB /* ColumnExpressionTests.swift */; };
56B7EE832863781300C0525F /* WALSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B7EE822863781300C0525F /* WALSnapshot.swift */; };
Expand Down Expand Up @@ -711,6 +712,7 @@
56AE64112229A53700AD1B0B /* HasOneThroughAssociation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HasOneThroughAssociation.swift; sourceTree = "<group>"; };
56AE6423222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasOneThroughSQLTests.swift; sourceTree = "<group>"; };
56AF746A1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleEscapingTests.swift; sourceTree = "<group>"; };
56AFEF2E29969F6E00CA1E51 /* TransactionClock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionClock.swift; sourceTree = "<group>"; };
56B021C81D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutablePersistableRecordPersistenceConflictPolicyTests.swift; sourceTree = "<group>"; };
56B14E7E1D4DAE54000BF4A3 /* RowFromDictionaryLiteralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFromDictionaryLiteralTests.swift; sourceTree = "<group>"; };
56B6EF55208CB4E3002F0ACB /* ColumnExpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnExpressionTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1467,6 +1469,7 @@
56A238781B9C75030082EB20 /* Statement.swift */,
566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */,
560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */,
56AFEF2E29969F6E00CA1E51 /* TransactionClock.swift */,
566B91321FA4D3810012D5B0 /* TransactionObserver.swift */,
56B7EE822863781300C0525F /* WALSnapshot.swift */,
5605F1471C672E4000235C62 /* Support */,
Expand Down Expand Up @@ -2098,6 +2101,7 @@
5657AB0F1D10899D006283EF /* URL.swift in Sources */,
560D924B1C672C4B00F4F92B /* TableRecord.swift in Sources */,
56DAA2DB1DE9C827006E10C8 /* Cursor.swift in Sources */,
56AFEF2F29969F6E00CA1E51 /* TransactionClock.swift in Sources */,
5674A6EB1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */,
56D91AA92205F2F100770D8D /* DatabasePromise.swift in Sources */,
56959629222C462D002CB7C9 /* HasManyThroughAssociation.swift in Sources */,
Expand Down
13 changes: 13 additions & 0 deletions GRDB/Core/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,19 @@ public struct Configuration {
/// ```
public var publicStatementArguments = false

/// The clock that feeds ``Database/transactionDate``.
///
/// The default clock is ``DefaultTransactionClock`` (which returns the
/// current date with `Date()`).
///
/// For example:
///
/// ```swift
/// var config = Configuration()
/// config.transactionClock = .custom { db in /* return some Date */ }
/// ```
public var transactionClock: TransactionClock = .default

// MARK: - Managing SQLite Connections

private var setups: [(Database) throws -> Void] = []
Expand Down
23 changes: 23 additions & 0 deletions GRDB/Core/Database+Statements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@ extension Database {
clearSchemaCache()
}

checkForAutocommitTransition()

// Database observation: cleanup
try observationBroker?.statementDidExecute(statement)
}
Expand All @@ -460,6 +462,8 @@ extension Database {
internalStatementCache.remove(statement)
publicStatementCache.remove(statement)

checkForAutocommitTransition()

// Extract values that may be modified by the user in their
// `TransactionObserver.databaseDidRollback(_:)` implementation
// (see below).
Expand All @@ -481,6 +485,25 @@ extension Database {
arguments: arguments,
publicStatementArguments: configuration.publicStatementArguments)
}

private func checkForAutocommitTransition() {
if sqlite3_get_autocommit(sqliteConnection) == 0 {
if autocommitState == .on {
// Record transaction date as soon as the connection leaves
// auto-commit mode.
// We grab a result, so that this failure is later reported
// whenever the user calls `Database.transactionDate`.
transactionDateResult = Result { try configuration.transactionClock.now(self) }
}
autocommitState = .off
} else {
if autocommitState == .off {
// Reset transaction date
transactionDateResult = nil
}
autocommitState = .on
}
}
}

/// A thread-unsafe statement cache
Expand Down
40 changes: 40 additions & 0 deletions GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_
/// - ``inTransaction(_:_:)``
/// - ``isInsideTransaction``
/// - ``rollback()``
/// - ``transactionDate``
/// - ``TransactionCompletion``
/// - ``TransactionKind``
///
Expand Down Expand Up @@ -276,6 +277,45 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
/// This cache is never cleared: we assume journal mode never changes.
var journalModeCache: String?

// MARK: - Transaction Date

enum AutocommitState {
case off
case on
}

/// Whether the last executed statement left the database is the auto-commit
/// mode or not.
var autocommitState = AutocommitState.on

/// The date of the current transaction, wrapped in a result that is an
/// error if there was an error grabbing this date when the transaction has
/// started.
///
/// Invariant: `transactionDateResult` is nil iff connection is not
/// inside a transaction.
var transactionDateResult: Result<Date, Error>?

/// The date of the current transaction.
///
/// It is constant at any point during a transaction.
///
/// When the database is not currently in a transaction, a new date is
/// returned on each call.
public var transactionDate: Date {
get throws {
// Check invariant: `transactionDateResult` is nil iff connection
// is not inside a transaction.
assert(isInsideTransaction || transactionDateResult == nil)

if let transactionDateResult {
return try transactionDateResult.get()
} else {
return try configuration.transactionClock.now(self)
}
}
}

// MARK: - Private properties

/// Support for ``Configuration/busyMode``.
Expand Down
64 changes: 64 additions & 0 deletions GRDB/Core/TransactionClock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation

/// A type that provides the moment of a transaction.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// ## Topics
///
/// ### Built-in Clocks
///
/// - ``DefaultTransactionClock``
/// - ``CustomTransactionClock``
public protocol TransactionClock {
/// Returns the date of the current transaction.
///
/// This function is called whenever a transaction starts - precisely
/// speaking, whenever the database connection leaves the auto-commit mode.
///
/// It is also called when the ``Database/transactionDate`` property is
/// called, and the database connection is not in a transaction.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/get_autocommit.html>
func now(_ db: Database) throws -> Date
}

extension TransactionClock where Self == DefaultTransactionClock {
/// Returns the default clock.
public static var `default`: Self { DefaultTransactionClock() }
}

extension TransactionClock where Self == CustomTransactionClock {
/// Returns a custom clock.
///
/// The provided closure is called whenever a transaction starts - precisely
/// speaking, whenever the database connection leaves the auto-commit mode.
///
/// It is also called when the ``Database/transactionDate`` property is
/// called, and the database connection is not in a transaction.
public static func custom(_ now: @escaping (Database) throws -> Date) -> Self {
CustomTransactionClock(now)
}
}

/// The default clock.
public struct DefaultTransactionClock: TransactionClock {
public func now(_ db: Database) throws -> Date {
// An opportunity to fetch transaction time from the database when
// SQLite supports the feature.
Date()
}
}

/// A custom transaction clock.
public struct CustomTransactionClock: TransactionClock {
let _now: (Database) throws -> Date

public init(_ now: @escaping (Database) throws -> Date) {
self._now = now
}

public func now(_ db: Database) throws -> Date {
try _now(db)
}
}
2 changes: 2 additions & 0 deletions GRDB/Documentation.docc/Extension/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ do {
- ``observesSuspensionNotifications``
- ``prepareDatabase(_:)``
- ``publicStatementArguments``
- ``transactionClock``
- ``TransactionClock``

### Configuring the Quality of Service

Expand Down
4 changes: 4 additions & 0 deletions GRDBCustom.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@
56A8C2461D1918EF0096E9D4 /* FoundationUUIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C21E1D1914110096E9D4 /* FoundationUUIDTests.swift */; };
56AE6428222AACE300AD1B0B /* AssociationHasOneThroughSQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE6426222AACE300AD1B0B /* AssociationHasOneThroughSQLTests.swift */; };
56AF746E1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AF746A1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift */; };
56AFEF3229969F7E00CA1E51 /* TransactionClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AFEF3029969F7E00CA1E51 /* TransactionClock.swift */; };
56B021CC1D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B021C81D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */; };
56B14E821D4DAE54000BF4A3 /* RowFromDictionaryLiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B14E7E1D4DAE54000BF4A3 /* RowFromDictionaryLiteralTests.swift */; };
56B6EF60208CB746002F0ACB /* ColumnExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B6EF5E208CB746002F0ACB /* ColumnExpressionTests.swift */; };
Expand Down Expand Up @@ -722,6 +723,7 @@
56A8C2361D1914790096E9D4 /* FoundationNSUUIDTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSUUIDTests.swift; sourceTree = "<group>"; };
56AE6426222AACE300AD1B0B /* AssociationHasOneThroughSQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasOneThroughSQLTests.swift; sourceTree = "<group>"; };
56AF746A1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleEscapingTests.swift; sourceTree = "<group>"; };
56AFEF3029969F7E00CA1E51 /* TransactionClock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionClock.swift; sourceTree = "<group>"; };
56B021C81D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutablePersistableRecordPersistenceConflictPolicyTests.swift; sourceTree = "<group>"; };
56B14E7E1D4DAE54000BF4A3 /* RowFromDictionaryLiteralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFromDictionaryLiteralTests.swift; sourceTree = "<group>"; };
56B6EF5E208CB746002F0ACB /* ColumnExpressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColumnExpressionTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1463,6 +1465,7 @@
56A238781B9C75030082EB20 /* Statement.swift */,
566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */,
560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */,
56AFEF3029969F7E00CA1E51 /* TransactionClock.swift */,
566B91321FA4D3810012D5B0 /* TransactionObserver.swift */,
56564F3828637C9900A19E9F /* WALSnapshot.swift */,
5605F1471C672E4000235C62 /* Support */,
Expand Down Expand Up @@ -1923,6 +1926,7 @@
5656A8852295BD56001FF3FF /* SQLOrdering.swift in Sources */,
F3BA808D1CFB2E75003DC1BA /* Migration.swift in Sources */,
F3BA80791CFB2E61003DC1BA /* DatabaseDateComponents.swift in Sources */,
56AFEF3229969F7E00CA1E51 /* TransactionClock.swift in Sources */,
5674A6EC1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */,
568ECB0D25D904CA00B71526 /* SQLSelection.swift in Sources */,
5657AB111D10899D006283EF /* URL.swift in Sources */,
Expand Down

0 comments on commit f649d9e

Please sign in to comment.