487 lines
18 KiB
Swift
487 lines
18 KiB
Swift
import XCTest
|
|
import GRDB
|
|
|
|
class TransactionDateTests: GRDBTestCase {
|
|
func testTransactionDateOutsideOfTransaction() throws {
|
|
let dates = [
|
|
Date.distantPast,
|
|
Date(),
|
|
Date.distantFuture,
|
|
]
|
|
var dateIterator = dates.makeIterator()
|
|
dbConfiguration.transactionClock = .custom { _ in
|
|
dateIterator.next()!
|
|
}
|
|
|
|
var collectedDates: [Date] = []
|
|
try makeDatabaseQueue().inDatabase { db in
|
|
try collectedDates.append(db.transactionDate)
|
|
try collectedDates.append(db.transactionDate)
|
|
try collectedDates.append(db.transactionDate)
|
|
}
|
|
XCTAssertEqual(collectedDates, dates)
|
|
}
|
|
|
|
func testTransactionDateInsideTransaction_commit() throws {
|
|
let dates = [
|
|
Date.distantPast,
|
|
Date(),
|
|
Date.distantFuture,
|
|
]
|
|
var dateIterator = dates.makeIterator()
|
|
dbConfiguration.transactionClock = .custom { _ in
|
|
dateIterator.next()!
|
|
}
|
|
|
|
var collectedDates: [Date] = []
|
|
try makeDatabaseQueue().inDatabase { db in
|
|
try collectedDates.append(db.transactionDate)
|
|
try db.execute(sql: "BEGIN")
|
|
try collectedDates.append(db.transactionDate)
|
|
try collectedDates.append(db.transactionDate)
|
|
try db.execute(sql: "COMMIT")
|
|
try collectedDates.append(db.transactionDate)
|
|
}
|
|
XCTAssertEqual(collectedDates, [dates[0], dates[1], dates[1], dates[2]])
|
|
}
|
|
|
|
func testTransactionDateInsideTransaction_rollback() throws {
|
|
let dates = [
|
|
Date.distantPast,
|
|
Date(),
|
|
Date.distantFuture,
|
|
]
|
|
var dateIterator = dates.makeIterator()
|
|
dbConfiguration.transactionClock = .custom { _ in
|
|
dateIterator.next()!
|
|
}
|
|
|
|
var collectedDates: [Date] = []
|
|
try makeDatabaseQueue().inDatabase { db in
|
|
try collectedDates.append(db.transactionDate)
|
|
try db.execute(sql: "BEGIN")
|
|
try collectedDates.append(db.transactionDate)
|
|
try collectedDates.append(db.transactionDate)
|
|
try db.execute(sql: "ROLLBACK")
|
|
try collectedDates.append(db.transactionDate)
|
|
}
|
|
XCTAssertEqual(collectedDates, [dates[0], dates[1], dates[1], dates[2]])
|
|
}
|
|
|
|
func testTransactionDateInsideTransaction_rollbackingError() throws {
|
|
let dates = [
|
|
Date.distantPast,
|
|
Date(),
|
|
Date.distantFuture,
|
|
]
|
|
var dateIterator = dates.makeIterator()
|
|
dbConfiguration.transactionClock = .custom { _ in
|
|
dateIterator.next()!
|
|
}
|
|
|
|
var collectedDates: [Date] = []
|
|
try makeDatabaseQueue().inDatabase { db in
|
|
try collectedDates.append(db.transactionDate)
|
|
try db.execute(sql: "BEGIN")
|
|
try collectedDates.append(db.transactionDate)
|
|
try collectedDates.append(db.transactionDate)
|
|
try? db.execute(sql: """
|
|
CREATE TABLE t(id INTEGER PRIMARY KEY ON CONFLICT ROLLBACK);
|
|
INSERT INTO t VALUES (1);
|
|
INSERT INTO t VALUES (1); -- fails and rollbacks
|
|
""")
|
|
try collectedDates.append(db.transactionDate)
|
|
}
|
|
XCTAssertEqual(collectedDates, [dates[0], dates[1], dates[1], dates[2]])
|
|
}
|
|
|
|
func test_TimestampedRecord_default_willInsert() throws {
|
|
struct Player: Codable, MutablePersistableRecord, FetchableRecord, TimestampedRecord {
|
|
var id: Int64?
|
|
var creationDate: Date?
|
|
var modificationDate: Date?
|
|
var name: String
|
|
|
|
mutating func didInsert(_ inserted: InsertionSuccess) {
|
|
id = inserted.rowID
|
|
}
|
|
}
|
|
|
|
var currentDate = Date.distantPast
|
|
dbConfiguration.transactionClock = .custom { _ in currentDate }
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write { db in
|
|
try db.create(table: "player") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("creationDate", .datetime).notNull()
|
|
t.column("modificationDate", .datetime).notNull()
|
|
t.column("name", .text).notNull()
|
|
}
|
|
}
|
|
|
|
currentDate = Date.distantPast
|
|
try dbQueue.write { db in
|
|
do {
|
|
var player = Player(name: "Arthur")
|
|
try player.insert(db)
|
|
XCTAssertEqual(player.creationDate, .distantPast)
|
|
XCTAssertEqual(player.modificationDate, .distantPast)
|
|
}
|
|
|
|
do {
|
|
let customDate = Date()
|
|
var player = Player(name: "Arthur")
|
|
player.creationDate = customDate
|
|
player.modificationDate = customDate
|
|
try player.insert(db)
|
|
XCTAssertEqual(player.creationDate, customDate)
|
|
XCTAssertEqual(player.modificationDate, customDate)
|
|
}
|
|
}
|
|
}
|
|
|
|
func test_TimestampedRecord_updateWithTimestamp() throws {
|
|
struct Player: Codable, MutablePersistableRecord, FetchableRecord, TimestampedRecord {
|
|
var id: Int64?
|
|
var creationDate: Date?
|
|
var modificationDate: Date?
|
|
var name: String
|
|
|
|
mutating func didInsert(_ inserted: InsertionSuccess) {
|
|
id = inserted.rowID
|
|
}
|
|
}
|
|
|
|
var currentDate = Date.distantPast
|
|
dbConfiguration.transactionClock = .custom { _ in currentDate }
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write { db in
|
|
try db.create(table: "player") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("creationDate", .datetime).notNull()
|
|
t.column("modificationDate", .datetime).notNull()
|
|
t.column("name", .text).notNull()
|
|
}
|
|
}
|
|
|
|
currentDate = Date.distantPast
|
|
try dbQueue.write { db in
|
|
var player = Player(name: "Arthur")
|
|
try player.insert(db)
|
|
}
|
|
|
|
let newTransactionDate = Date()
|
|
currentDate = newTransactionDate
|
|
try dbQueue.write { db in
|
|
var player = try Player.find(db, key: 1)
|
|
|
|
player.name = "Barbara"
|
|
try player.updateWithTimestamp(db)
|
|
XCTAssertEqual(player.creationDate, .distantPast)
|
|
XCTAssertEqual(player.modificationDate, newTransactionDate)
|
|
|
|
try player.updateWithTimestamp(db, modificationDate: .distantFuture)
|
|
XCTAssertEqual(player.creationDate, .distantPast)
|
|
XCTAssertEqual(player.modificationDate, .distantFuture)
|
|
}
|
|
}
|
|
|
|
func test_TimestampedRecord_updateChangesWithTimestamp() throws {
|
|
struct Player: Codable, MutablePersistableRecord, FetchableRecord, TimestampedRecord {
|
|
var id: Int64?
|
|
var creationDate: Date?
|
|
var modificationDate: Date?
|
|
var name: String
|
|
|
|
mutating func didInsert(_ inserted: InsertionSuccess) {
|
|
id = inserted.rowID
|
|
}
|
|
}
|
|
|
|
var currentDate = Date.distantPast
|
|
dbConfiguration.transactionClock = .custom { _ in currentDate }
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write { db in
|
|
try db.create(table: "player") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("creationDate", .datetime).notNull()
|
|
t.column("modificationDate", .datetime).notNull()
|
|
t.column("name", .text).notNull()
|
|
}
|
|
}
|
|
|
|
currentDate = Date.distantPast
|
|
try dbQueue.write { db in
|
|
var player = Player(name: "Arthur")
|
|
try player.insert(db)
|
|
}
|
|
|
|
let newTransactionDate = Date()
|
|
currentDate = newTransactionDate
|
|
try dbQueue.write { db in
|
|
var player = try Player.find(db, key: 1)
|
|
|
|
let changed = try player.updateChangesWithTimestamp(db) {
|
|
$0.name = "Barbara"
|
|
}
|
|
XCTAssertTrue(changed)
|
|
XCTAssertEqual(player.creationDate, .distantPast)
|
|
XCTAssertEqual(player.modificationDate, newTransactionDate)
|
|
}
|
|
|
|
try dbQueue.write { db in
|
|
var player = try Player.find(db, key: 1)
|
|
|
|
let changed = try player.updateChangesWithTimestamp(db) {
|
|
$0.name = "Barbara"
|
|
}
|
|
XCTAssertFalse(changed)
|
|
}
|
|
}
|
|
|
|
func test_TimestampedRecord_touch() throws {
|
|
struct Player: Codable, MutablePersistableRecord, FetchableRecord, TimestampedRecord {
|
|
var id: Int64?
|
|
var creationDate: Date?
|
|
var modificationDate: Date?
|
|
var name: String
|
|
|
|
mutating func didInsert(_ inserted: InsertionSuccess) {
|
|
id = inserted.rowID
|
|
}
|
|
}
|
|
|
|
var currentDate = Date.distantPast
|
|
dbConfiguration.transactionClock = .custom { _ in currentDate }
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write { db in
|
|
try db.create(table: "player") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("creationDate", .datetime).notNull()
|
|
t.column("modificationDate", .datetime).notNull()
|
|
t.column("name", .text).notNull()
|
|
}
|
|
|
|
var player = Player(name: "Arthur")
|
|
try player.insert(db)
|
|
}
|
|
|
|
let newTransactionDate = Date()
|
|
currentDate = newTransactionDate
|
|
try dbQueue.write { db in
|
|
var player = try Player.find(db, key: 1)
|
|
try player.touch(db)
|
|
XCTAssertEqual(player.modificationDate, newTransactionDate)
|
|
|
|
try player.touch(db, modificationDate: .distantFuture)
|
|
XCTAssertEqual(player.modificationDate, .distantFuture)
|
|
}
|
|
}
|
|
|
|
func test_TimestampedRecord_struct_with_customized_willInsert() throws {
|
|
struct Player: Codable, TimestampedRecord, FetchableRecord {
|
|
var id: Int64?
|
|
var creationDate: Date?
|
|
var modificationDate: Date?
|
|
var name: String
|
|
var isInserted = false // transient
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case creationDate
|
|
case modificationDate
|
|
case name
|
|
}
|
|
|
|
mutating func willInsert(_ db: Database) throws {
|
|
isInserted = true
|
|
try initializeTimestamps(db)
|
|
}
|
|
|
|
mutating func didInsert(_ inserted: InsertionSuccess) {
|
|
id = inserted.rowID
|
|
}
|
|
}
|
|
|
|
var currentDate = Date.distantPast
|
|
dbConfiguration.transactionClock = .custom { _ in currentDate }
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write { db in
|
|
try db.create(table: "player") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("creationDate", .datetime).notNull()
|
|
t.column("modificationDate", .datetime).notNull()
|
|
t.column("name", .text).notNull()
|
|
}
|
|
}
|
|
|
|
currentDate = Date.distantPast
|
|
try dbQueue.write { db in
|
|
var player = Player(name: "Arthur", isInserted: false)
|
|
try player.insert(db)
|
|
XCTAssertTrue(player.isInserted)
|
|
XCTAssertEqual(player.creationDate, .distantPast)
|
|
XCTAssertEqual(player.modificationDate, .distantPast)
|
|
}
|
|
}
|
|
|
|
func test_TimestampedRecord_class_with_non_mutating_willInsert() throws {
|
|
class Player: Codable, TimestampedRecord, PersistableRecord, FetchableRecord {
|
|
var id: Int64?
|
|
var creationDate: Date?
|
|
var modificationDate: Date?
|
|
var name: String
|
|
|
|
init(id: Int64? = nil, creationDate: Date? = nil, modificationDate: Date? = nil, name: String) {
|
|
self.id = id
|
|
self.creationDate = creationDate
|
|
self.modificationDate = modificationDate
|
|
self.name = name
|
|
}
|
|
|
|
func willInsert(_ db: Database) throws {
|
|
// Can't call initializeTimestamps because it is mutating
|
|
if creationDate == nil {
|
|
creationDate = try db.transactionDate
|
|
}
|
|
if modificationDate == nil {
|
|
modificationDate = try db.transactionDate
|
|
}
|
|
}
|
|
|
|
func didInsert(_ inserted: InsertionSuccess) {
|
|
id = inserted.rowID
|
|
}
|
|
}
|
|
|
|
var currentDate = Date.distantPast
|
|
dbConfiguration.transactionClock = .custom { _ in currentDate }
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write { db in
|
|
try db.create(table: "player") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("creationDate", .datetime).notNull()
|
|
t.column("modificationDate", .datetime).notNull()
|
|
t.column("name", .text).notNull()
|
|
}
|
|
}
|
|
|
|
currentDate = Date.distantPast
|
|
try dbQueue.write { db in
|
|
let player = Player(name: "Arthur")
|
|
try player.insert(db)
|
|
XCTAssertEqual(player.creationDate, .distantPast)
|
|
XCTAssertEqual(player.modificationDate, .distantPast)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The protocol in RecordTimestamps.md
|
|
|
|
/// A record type that tracks its creation and modification dates. See
|
|
/// <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordtimestamps>
|
|
protocol TimestampedRecord: MutablePersistableRecord {
|
|
var creationDate: Date? { get set }
|
|
var modificationDate: Date? { get set }
|
|
}
|
|
|
|
extension TimestampedRecord {
|
|
/// By default, `TimestampedRecord` types set `creationDate` and
|
|
/// `modificationDate` to the transaction date, if they are nil,
|
|
/// before insertion.
|
|
///
|
|
/// `TimestampedRecord` types that customize the `willInsert`
|
|
/// persistence callback should call `initializeTimestamps` from
|
|
/// their implementation.
|
|
mutating func willInsert(_ db: Database) throws {
|
|
try initializeTimestamps(db)
|
|
}
|
|
|
|
/// Sets `creationDate` and `modificationDate` to the transaction date,
|
|
/// if they are nil.
|
|
///
|
|
/// It is called automatically before insertion, if your type does not
|
|
/// customize the `willInsert` persistence callback. If you customize
|
|
/// this callback, call `initializeTimestamps` from your implementation.
|
|
mutating func initializeTimestamps(_ db: Database) throws {
|
|
if creationDate == nil {
|
|
creationDate = try db.transactionDate
|
|
}
|
|
if modificationDate == nil {
|
|
modificationDate = try db.transactionDate
|
|
}
|
|
}
|
|
|
|
/// Sets `modificationDate`, and executes an `UPDATE` statement
|
|
/// on all columns.
|
|
///
|
|
/// - parameter modificationDate: The modification date. If nil, the
|
|
/// transaction date is used.
|
|
mutating func updateWithTimestamp(_ db: Database, modificationDate: Date? = nil) throws {
|
|
self.modificationDate = try modificationDate ?? db.transactionDate
|
|
try update(db)
|
|
}
|
|
|
|
/// Modifies the record according to the provided `modify` closure, and,
|
|
/// if and only if the record was modified, sets `modificationDate` and
|
|
/// executes an `UPDATE` statement that updates the modified columns.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// var player = Player.find(db, id: 1)
|
|
/// let modified = try player.updateChangesWithTimestamp(db) {
|
|
/// $0.score = 1000
|
|
/// }
|
|
/// if modified {
|
|
/// print("player was modified")
|
|
/// } else {
|
|
/// print("player was not modified")
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - modificationDate: The modification date. If nil, the
|
|
/// transaction date is used.
|
|
/// - modify: A closure that modifies the record.
|
|
/// - returns: Whether the record was changed and updated.
|
|
@discardableResult
|
|
mutating func updateChangesWithTimestamp(
|
|
_ db: Database,
|
|
modificationDate: Date? = nil,
|
|
modify: (inout Self) -> Void)
|
|
throws -> Bool
|
|
{
|
|
// Grab the changes performed by `modify`
|
|
let initialChanges = try databaseChanges(modify: modify)
|
|
if initialChanges.isEmpty {
|
|
return false
|
|
}
|
|
|
|
// Update modification date and grab its column name
|
|
let dateChanges = try databaseChanges(modify: {
|
|
$0.modificationDate = try modificationDate ?? db.transactionDate
|
|
})
|
|
|
|
// Update the modified columns
|
|
let modifiedColumns = Set(initialChanges.keys).union(dateChanges.keys)
|
|
try update(db, columns: modifiedColumns)
|
|
return true
|
|
}
|
|
|
|
/// Sets `modificationDate`, and executes an `UPDATE` statement that
|
|
/// updates the `modificationDate` column, if and only if the record
|
|
/// was modified.
|
|
///
|
|
/// - parameter modificationDate: The modification date. If nil, the
|
|
/// transaction date is used.
|
|
mutating func touch(_ db: Database, modificationDate: Date? = nil) throws {
|
|
try updateChanges(db) {
|
|
$0.modificationDate = try modificationDate ?? db.transactionDate
|
|
}
|
|
}
|
|
}
|