From 4935c6f2582aee56c17995970ad7011037a52467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 1 Mar 2019 08:24:00 +0100 Subject: [PATCH 1/4] Rename ValueScheduling.onQueue(_:startImmediately:) to .async(onQueue:startImmediately:) --- GRDB/Core/DatabaseSnapshot.swift | 2 +- GRDB/Core/DatabaseWriter.swift | 2 +- GRDB/Fixit/GRDB-4.0.swift | 7 +++++++ GRDB/ValueObservation/ValueObservation.swift | 4 ++-- Tests/GRDBTests/ValueObservationSchedulingTests.swift | 6 +++--- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 4a010d9ca9..fc7b28adcd 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -116,7 +116,7 @@ extension DatabaseSnapshot { } } } - case let .onQueue(queue, startImmediately: startImmediately): + case let .async(onQueue: queue, startImmediately: startImmediately): if startImmediately { if let value = try unsafeReentrantRead(observation.initialValue) { queue.async { diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index 68265745f1..5366341df9 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -210,7 +210,7 @@ extension DatabaseWriter { DispatchQueue.main.async { onChange(value) } } } - case let .onQueue(queue, startImmediately: startImmediately): + case let .async(onQueue: queue, startImmediately: startImmediately): if startImmediately { if let value = try reducer.initialValue(db, requiresWriteAccess: observation.requiresWriteAccess) { queue.async { onChange(value) } diff --git a/GRDB/Fixit/GRDB-4.0.swift b/GRDB/Fixit/GRDB-4.0.swift index 4cf4240c53..7a84bdb231 100644 --- a/GRDB/Fixit/GRDB-4.0.swift +++ b/GRDB/Fixit/GRDB-4.0.swift @@ -116,3 +116,10 @@ extension DatabaseValue { @available(*, unavailable) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T: DatabaseValueConvertible { preconditionFailure() } } + +extension ValueScheduling { + @available(*, unavailable, renamed: "async(onQueue:startImmediately:)") + public static func onQueue(_ queue: DispatchQueue, startImmediately: Bool) -> ValueScheduling { + return .async(onQueue: queue, startImmediately: startImmediately) + } +} diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 5822159277..5f29de1adb 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -40,7 +40,7 @@ public enum ValueScheduling { /// /// An initial value is fetched and notified if `startImmediately` /// is true. - case onQueue(DispatchQueue, startImmediately: Bool) + case async(onQueue: DispatchQueue, startImmediately: Bool) /// Values are not all notified on the same dispatch queue. /// @@ -158,7 +158,7 @@ public struct ValueObservation { switch scheduling { case .mainQueue: return DispatchQueue.main - case .onQueue(let queue, startImmediately: _): + case let .async(onQueue: queue, startImmediately: _): return queue case .unsafe: return nil diff --git a/Tests/GRDBTests/ValueObservationSchedulingTests.swift b/Tests/GRDBTests/ValueObservationSchedulingTests.swift index 7cc8c7163f..0901316cca 100644 --- a/Tests/GRDBTests/ValueObservationSchedulingTests.swift +++ b/Tests/GRDBTests/ValueObservationSchedulingTests.swift @@ -160,7 +160,7 @@ class ValueObservationSchedulingTests: GRDBTestCase { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) observation.extent = .databaseLifetime - observation.scheduling = .onQueue(queue, startImmediately: true) + observation.scheduling = .async(onQueue: queue, startImmediately: true) _ = try observation.start(in: dbWriter) { count in XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) @@ -198,7 +198,7 @@ class ValueObservationSchedulingTests: GRDBTestCase { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) observation.extent = .databaseLifetime - observation.scheduling = .onQueue(queue, startImmediately: false) + observation.scheduling = .async(onQueue: queue, startImmediately: false) _ = try observation.start(in: dbWriter) { count in XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) @@ -238,7 +238,7 @@ class ValueObservationSchedulingTests: GRDBTestCase { value: { $0 }) var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) observation.extent = .databaseLifetime - observation.scheduling = .onQueue(queue, startImmediately: false) + observation.scheduling = .async(onQueue: queue, startImmediately: false) _ = try observation.start( in: dbWriter, From e966544aeb8a2c81c65cabd7248cc26e128b8d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 1 Mar 2019 09:04:00 +0100 Subject: [PATCH 2/4] Remove ValueObservation.extent --- GRDB/Core/DatabaseWriter.swift | 2 +- GRDB/Fixit/GRDB-4.0.swift | 10 +- .../ValueObservation+MapReducer.swift | 1 - GRDB/ValueObservation/ValueObservation.swift | 7 - .../ValueObservationCombineTests.swift | 396 +++++++++--------- .../ValueObservationCompactMapTests.swift | 22 +- .../ValueObservationCountTests.swift | 34 +- ...rvationDatabaseValueConvertibleTests.swift | 180 ++++---- .../ValueObservationExtentTests.swift | 84 +--- .../ValueObservationFetchTests.swift | 44 +- .../GRDBTests/ValueObservationMapTests.swift | 22 +- .../ValueObservationReadonlyTests.swift | 34 +- .../ValueObservationRecordTests.swift | 80 ++-- .../ValueObservationReducerTests.swift | 212 +++++----- .../GRDBTests/ValueObservationRowTests.swift | 66 +-- .../ValueObservationSchedulingTests.swift | 162 +++---- 16 files changed, 634 insertions(+), 722 deletions(-) diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index 5366341df9..a3090e6e52 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -231,7 +231,7 @@ extension DatabaseWriter { notificationQueue: observation.notificationQueue, onError: onError, onChange: onChange) - db.add(transactionObserver: valueObserver, extent: observation.extent) + db.add(transactionObserver: valueObserver, extent: .observerLifetime) return valueObserver } diff --git a/GRDB/Fixit/GRDB-4.0.swift b/GRDB/Fixit/GRDB-4.0.swift index 7a84bdb231..c454ef8231 100644 --- a/GRDB/Fixit/GRDB-4.0.swift +++ b/GRDB/Fixit/GRDB-4.0.swift @@ -119,7 +119,13 @@ extension DatabaseValue { extension ValueScheduling { @available(*, unavailable, renamed: "async(onQueue:startImmediately:)") - public static func onQueue(_ queue: DispatchQueue, startImmediately: Bool) -> ValueScheduling { - return .async(onQueue: queue, startImmediately: startImmediately) + public static func onQueue(_ queue: DispatchQueue, startImmediately: Bool) -> ValueScheduling { preconditionFailure() } +} + +extension ValueObservation { + @available(*, unavailable, message: "Observation extent is controlled by the lifetime of observers returned by the start() method.") + public var extent: Database.TransactionObservationExtent { + get { preconditionFailure() } + set { preconditionFailure() } } } diff --git a/GRDB/ValueObservation/ValueObservation+MapReducer.swift b/GRDB/ValueObservation/ValueObservation+MapReducer.swift index baea470b8c..0f9705c3b9 100644 --- a/GRDB/ValueObservation/ValueObservation+MapReducer.swift +++ b/GRDB/ValueObservation/ValueObservation+MapReducer.swift @@ -7,7 +7,6 @@ extension ValueObservation { var observation = ValueObservation( tracking: observedRegion, reducer: { db in try transform(db, makeReducer(db)) }) - observation.extent = extent observation.scheduling = scheduling observation.requiresWriteAccess = requiresWriteAccess return observation diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 5f29de1adb..c263e73a51 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -89,13 +89,6 @@ public struct ValueObservation { /// observation is less efficient than a read-only observation. public var requiresWriteAccess: Bool = false - // TODO: replace ValueObservation.extent - /// The extent of the database observation. The default is - /// `.observerLifetime`: the observation lasts until the - /// observer returned by the `start(in:onError:onChange:)` method - /// is deallocated. - public var extent = Database.TransactionObservationExtent.observerLifetime - /// `scheduling` controls how fresh values are notified. Default /// is `.mainQueue`. /// diff --git a/Tests/GRDBTests/ValueObservationCombineTests.swift b/Tests/GRDBTests/ValueObservationCombineTests.swift index 77418566af..967e29dd33 100644 --- a/Tests/GRDBTests/ValueObservationCombineTests.swift +++ b/Tests/GRDBTests/ValueObservationCombineTests.swift @@ -31,43 +31,43 @@ class ValueObservationCombineTests: GRDBTestCase { struct T2: TableRecord { } let observation1 = ValueObservation.trackingCount(T1.all()) let observation2 = ValueObservation.trackingCount(T2.all()) - var observation = ValueObservation.combine(observation1, observation2) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { value in + let observation = ValueObservation.combine(observation1, observation2) + let observer = try observation.start(in: dbQueue) { value in values.append(value) notificationExpectation.fulfill() } - - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t1") - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t2") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t1") + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t2") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t1") + try db.execute(sql: "DELETE FROM t2") + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(values.count, 4) + XCTAssert(values[0] == (0, 0)) + XCTAssert(values[1] == (1, 0)) + XCTAssert(values[2] == (1, 1)) + XCTAssert(values[3] == (2, 2)) } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t1") - try db.execute(sql: "DELETE FROM t2") - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - } - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(values.count, 4) - XCTAssert(values[0] == (0, 0)) - XCTAssert(values[1] == (1, 0)) - XCTAssert(values[2] == (1, 1)) - XCTAssert(values[3] == (2, 2)) } func testCombine3() throws { @@ -91,54 +91,54 @@ class ValueObservationCombineTests: GRDBTestCase { let observation1 = ValueObservation.trackingCount(T1.all()) let observation2 = ValueObservation.trackingCount(T2.all()) let observation3 = ValueObservation.trackingCount(T3.all()) - var observation = ValueObservation.combine(observation1, observation2, observation3) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { value in + let observation = ValueObservation.combine(observation1, observation2, observation3) + let observer = try observation.start(in: dbQueue) { value in values.append(value) notificationExpectation.fulfill() } - - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t1") + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t2") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t3") + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t1") + try db.execute(sql: "DELETE FROM t2") + try db.execute(sql: "DELETE FROM t3") + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + } + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(values.count, 5) + XCTAssert(values[0] == (0, 0, 0)) + XCTAssert(values[1] == (1, 0, 0)) + XCTAssert(values[2] == (1, 1, 0)) + XCTAssert(values[3] == (1, 1, 1)) + XCTAssert(values[4] == (2, 2, 2)) } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t1") - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t2") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t3") - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t1") - try db.execute(sql: "DELETE FROM t2") - try db.execute(sql: "DELETE FROM t3") - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - } - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(values.count, 5) - XCTAssert(values[0] == (0, 0, 0)) - XCTAssert(values[1] == (1, 0, 0)) - XCTAssert(values[2] == (1, 1, 0)) - XCTAssert(values[3] == (1, 1, 1)) - XCTAssert(values[4] == (2, 2, 2)) } func testCombine4() throws { @@ -165,65 +165,65 @@ class ValueObservationCombineTests: GRDBTestCase { let observation2 = ValueObservation.trackingCount(T2.all()) let observation3 = ValueObservation.trackingCount(T3.all()) let observation4 = ValueObservation.trackingCount(T4.all()) - var observation = ValueObservation.combine(observation1, observation2, observation3, observation4) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { value in + let observation = ValueObservation.combine(observation1, observation2, observation3, observation4) + let observer = try observation.start(in: dbQueue) { value in values.append(value) notificationExpectation.fulfill() } - - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t1") + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t2") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t3") + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t4") + try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t1") + try db.execute(sql: "DELETE FROM t2") + try db.execute(sql: "DELETE FROM t3") + try db.execute(sql: "DELETE FROM t4") + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") + } + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(values.count, 6) + XCTAssert(values[0] == (0, 0, 0, 0)) + XCTAssert(values[1] == (1, 0, 0, 0)) + XCTAssert(values[2] == (1, 1, 0, 0)) + XCTAssert(values[3] == (1, 1, 1, 0)) + XCTAssert(values[4] == (1, 1, 1, 1)) + XCTAssert(values[5] == (2, 2, 2, 2)) } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t1") - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t2") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t3") - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t4") - try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t1") - try db.execute(sql: "DELETE FROM t2") - try db.execute(sql: "DELETE FROM t3") - try db.execute(sql: "DELETE FROM t4") - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") - } - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(values.count, 6) - XCTAssert(values[0] == (0, 0, 0, 0)) - XCTAssert(values[1] == (1, 0, 0, 0)) - XCTAssert(values[2] == (1, 1, 0, 0)) - XCTAssert(values[3] == (1, 1, 1, 0)) - XCTAssert(values[4] == (1, 1, 1, 1)) - XCTAssert(values[5] == (2, 2, 2, 2)) } func testCombine5() throws { @@ -253,76 +253,76 @@ class ValueObservationCombineTests: GRDBTestCase { let observation3 = ValueObservation.trackingCount(T3.all()) let observation4 = ValueObservation.trackingCount(T4.all()) let observation5 = ValueObservation.trackingCount(T5.all()) - var observation = ValueObservation.combine(observation1, observation2, observation3, observation4, observation5) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { value in + let observation = ValueObservation.combine(observation1, observation2, observation3, observation4, observation5) + let observer = try observation.start(in: dbQueue) { value in values.append(value) notificationExpectation.fulfill() } - - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t5 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t1") - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t2") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t3") - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t4") - try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t5") - try db.execute(sql: "INSERT INTO t5 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "DELETE FROM t1") - try db.execute(sql: "DELETE FROM t2") - try db.execute(sql: "DELETE FROM t3") - try db.execute(sql: "DELETE FROM t4") - try db.execute(sql: "DELETE FROM t5") - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t5 DEFAULT VALUES") - } - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t5 DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t5 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t1") + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t2") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t3") + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t4") + try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t5") + try db.execute(sql: "INSERT INTO t5 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "DELETE FROM t1") + try db.execute(sql: "DELETE FROM t2") + try db.execute(sql: "DELETE FROM t3") + try db.execute(sql: "DELETE FROM t4") + try db.execute(sql: "DELETE FROM t5") + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t5 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO t1 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t2 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t3 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t4 DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t5 DEFAULT VALUES") + } + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(values.count, 7) + XCTAssert(values[0] == (0, 0, 0, 0, 0)) + XCTAssert(values[1] == (1, 0, 0, 0, 0)) + XCTAssert(values[2] == (1, 1, 0, 0, 0)) + XCTAssert(values[3] == (1, 1, 1, 0, 0)) + XCTAssert(values[4] == (1, 1, 1, 1, 0)) + XCTAssert(values[5] == (1, 1, 1, 1, 1)) + XCTAssert(values[6] == (2, 2, 2, 2, 2)) } - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(values.count, 7) - XCTAssert(values[0] == (0, 0, 0, 0, 0)) - XCTAssert(values[1] == (1, 0, 0, 0, 0)) - XCTAssert(values[2] == (1, 1, 0, 0, 0)) - XCTAssert(values[3] == (1, 1, 1, 0, 0)) - XCTAssert(values[4] == (1, 1, 1, 1, 0)) - XCTAssert(values[5] == (1, 1, 1, 1, 1)) - XCTAssert(values[6] == (2, 2, 2, 2, 2)) } func testHeterogeneusCombine2() throws { diff --git a/Tests/GRDBTests/ValueObservationCompactMapTests.swift b/Tests/GRDBTests/ValueObservationCompactMapTests.swift index 24bf04bc97..1191d93ce6 100644 --- a/Tests/GRDBTests/ValueObservationCompactMapTests.swift +++ b/Tests/GRDBTests/ValueObservationCompactMapTests.swift @@ -33,27 +33,27 @@ class ValueObservationCompactMapTests: GRDBTestCase { }) // Create an observation - var observation = ValueObservation + let observation = ValueObservation .tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) .compactMap { count -> String? in if count % 2 == 0 { return nil } return "\(count)" } - observation.extent = .databaseLifetime // Start observation - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.writeWithoutTransaction { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, ["1", "3"]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, ["1", "3"]) } try test(makeDatabaseQueue()) @@ -62,12 +62,10 @@ class ValueObservationCompactMapTests: GRDBTestCase { func testCompactMapPreservesConfiguration() { var observation = ValueObservation.tracking(DatabaseRegion(), fetch: { _ in }) - observation.extent = .nextTransaction observation.requiresWriteAccess = true observation.scheduling = .unsafe(startImmediately: true) let mappedObservation = observation.compactMap { _ in } - XCTAssertEqual(mappedObservation.extent, observation.extent) XCTAssertEqual(mappedObservation.requiresWriteAccess, observation.requiresWriteAccess) switch mappedObservation.scheduling { case .unsafe: diff --git a/Tests/GRDBTests/ValueObservationCountTests.swift b/Tests/GRDBTests/ValueObservationCountTests.swift index 83f5a3ba19..282df88b68 100644 --- a/Tests/GRDBTests/ValueObservationCountTests.swift +++ b/Tests/GRDBTests/ValueObservationCountTests.swift @@ -23,27 +23,27 @@ class ValueObservationCountTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 5 struct T: TableRecord { } - var observation = ValueObservation.trackingCount(T.all()) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { count in + let observation = ValueObservation.trackingCount(T.all()) + let observer = try observation.start(in: dbQueue) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbQueue.inDatabase { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") // +1 - try db.execute(sql: "UPDATE t SET id = id") // = - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") // +1 - try db.inTransaction { // +1 - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "DELETE FROM t WHERE id = 1") - return .commit + try withExtendedLifetime(observer) { + try dbQueue.inDatabase { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") // +1 + try db.execute(sql: "UPDATE t SET id = id") // = + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") // +1 + try db.inTransaction { // +1 + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "DELETE FROM t WHERE id = 1") + return .commit + } + try db.execute(sql: "DELETE FROM t WHERE id = 2") // -1 } - try db.execute(sql: "DELETE FROM t WHERE id = 2") // -1 + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1, 2, 3, 2]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1, 2, 3, 2]) } } diff --git a/Tests/GRDBTests/ValueObservationDatabaseValueConvertibleTests.swift b/Tests/GRDBTests/ValueObservationDatabaseValueConvertibleTests.swift index d278454b1b..d5b9f2795a 100644 --- a/Tests/GRDBTests/ValueObservationDatabaseValueConvertibleTests.swift +++ b/Tests/GRDBTests/ValueObservationDatabaseValueConvertibleTests.swift @@ -37,31 +37,31 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var observation = ValueObservation.trackingAll(SQLRequest(sql: "SELECT name FROM t ORDER BY id")) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { names in + let observation = ValueObservation.trackingAll(SQLRequest(sql: "SELECT name FROM t ORDER BY id")) + let observer = try observation.start(in: dbQueue) { names in results.append(names) notificationExpectation.fulfill() } - - try dbQueue.inDatabase { db in - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 - try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = - try db.inTransaction { // +1 - try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") - try db.execute(sql: "DELETE FROM t WHERE id = 3") - return .commit + try withExtendedLifetime(observer) { + try dbQueue.inDatabase { db in + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 + try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = + try db.inTransaction { // +1 + try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") + try db.execute(sql: "DELETE FROM t WHERE id = 3") + return .commit + } + try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 } - try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(results.map { $0.map { $0.rawValue }}, [ + [], + ["foo"], + ["foo", "bar"], + ["bar"]]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results.map { $0.map { $0.rawValue }}, [ - [], - ["foo"], - ["foo", "bar"], - ["bar"]]) } func testOne() throws { @@ -73,39 +73,39 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 7 - var observation = ValueObservation.trackingOne(SQLRequest(sql: "SELECT name FROM t ORDER BY id DESC")) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { name in + let observation = ValueObservation.trackingOne(SQLRequest(sql: "SELECT name FROM t ORDER BY id DESC")) + let observer = try observation.start(in: dbQueue) { name in results.append(name) notificationExpectation.fulfill() } - - try dbQueue.inDatabase { db in - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") - try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") - try db.inTransaction { - try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") - try db.execute(sql: "DELETE FROM t WHERE id = 3") - return .commit + try withExtendedLifetime(observer) { + try dbQueue.inDatabase { db in + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") + try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") + try db.inTransaction { + try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") + try db.execute(sql: "DELETE FROM t WHERE id = 3") + return .commit + } + try db.execute(sql: "DELETE FROM t") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'baz')") + try db.execute(sql: "UPDATE t SET name = NULL") + try db.execute(sql: "DELETE FROM t") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, NULL)") + try db.execute(sql: "UPDATE t SET name = 'qux'") } - try db.execute(sql: "DELETE FROM t") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'baz')") - try db.execute(sql: "UPDATE t SET name = NULL") - try db.execute(sql: "DELETE FROM t") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, NULL)") - try db.execute(sql: "UPDATE t SET name = 'qux'") + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(results.map { $0.map { $0.rawValue }}, [ + nil, + "foo", + "bar", + nil, + "baz", + nil, + "qux"]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results.map { $0.map { $0.rawValue }}, [ - nil, - "foo", - "bar", - nil, - "baz", - nil, - "qux"]) } func testAllOptional() throws { @@ -117,31 +117,31 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var observation = ValueObservation.trackingAll(SQLRequest(sql: "SELECT name FROM t ORDER BY id")) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { names in + let observation = ValueObservation.trackingAll(SQLRequest(sql: "SELECT name FROM t ORDER BY id")) + let observer = try observation.start(in: dbQueue) { names in results.append(names) notificationExpectation.fulfill() } - - try dbQueue.inDatabase { db in - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 - try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = - try db.inTransaction { // +1 - try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, NULL)") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") - try db.execute(sql: "DELETE FROM t WHERE id = 3") - return .commit + try withExtendedLifetime(observer) { + try dbQueue.inDatabase { db in + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 + try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = + try db.inTransaction { // +1 + try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, NULL)") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") + try db.execute(sql: "DELETE FROM t WHERE id = 3") + return .commit + } + try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 } - try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(results.map { $0.map { $0?.rawValue }}, [ + [], + ["foo"], + ["foo", nil], + [nil]]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results.map { $0.map { $0?.rawValue }}, [ - [], - ["foo"], - ["foo", nil], - [nil]]) } func testViewOptimization() throws { @@ -168,33 +168,33 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { // Test that view v is not included in the observed region. // This optimization helps observation of views that feed from a // single table. - var observation = ValueObservation.trackingAll(request) - observation.extent = .databaseLifetime - let transactionObserver = try observation.start(in: dbQueue) { names in + let observation = ValueObservation.trackingAll(request) + let observer = try observation.start(in: dbQueue) { names in results.append(names) notificationExpectation.fulfill() } - let valueObserver = transactionObserver as! ValueObserver>> + let valueObserver = observer as! ValueObserver>> XCTAssertEqual(valueObserver.region.description, "t(id,name)") // view is not tracked - - // Test view observation - try dbQueue.inDatabase { db in - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 - try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = - try db.inTransaction { // +1 - try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") - try db.execute(sql: "DELETE FROM t WHERE id = 3") - return .commit + try withExtendedLifetime(observer) { + // Test view observation + try dbQueue.inDatabase { db in + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 + try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = + try db.inTransaction { // +1 + try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") + try db.execute(sql: "DELETE FROM t WHERE id = 3") + return .commit + } + try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 } - try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(results.map { $0.map { $0.rawValue }}, [ + [], + ["foo"], + ["foo", "bar"], + ["bar"]]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results.map { $0.map { $0.rawValue }}, [ - [], - ["foo"], - ["foo", "bar"], - ["bar"]]) } } diff --git a/Tests/GRDBTests/ValueObservationExtentTests.swift b/Tests/GRDBTests/ValueObservationExtentTests.swift index 6bb531bd0c..ff1a551b39 100644 --- a/Tests/GRDBTests/ValueObservationExtentTests.swift +++ b/Tests/GRDBTests/ValueObservationExtentTests.swift @@ -13,48 +13,6 @@ import XCTest #endif class ValueObservationExtentTests: GRDBTestCase { - func testDefaultExtentIsObserverLifetime() { - let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in }) - XCTAssertEqual(observation.extent, .observerLifetime) - } - - func testExtentDatabaseLifetime() throws { - // We need something to change - let dbQueue = try makeDatabaseQueue() - try dbQueue.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } - - // Track reducer process - let notificationExpectation = expectation(description: "notification") - notificationExpectation.assertForOverFulfill = true - notificationExpectation.expectedFulfillmentCount = 3 - - // A reducer - let reducer = AnyValueReducer( - fetch: { _ in }, - value: { $0 }) - - // Create an observation - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .databaseLifetime - - // Start observation - _ = try observation.start(in: dbQueue) { - notificationExpectation.fulfill() - } - - // notified - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - } - - // notified - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - } - - waitForExpectations(timeout: 1, handler: nil) - } - func testExtentObserverLifetime() throws { // We need something to change let dbQueue = try makeDatabaseQueue() @@ -72,8 +30,7 @@ class ValueObservationExtentTests: GRDBTestCase { value: { $0 }) // Create an observation - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .observerLifetime + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) // Start observation and deallocate observer after second change var observer: TransactionObserver? @@ -101,43 +58,4 @@ class ValueObservationExtentTests: GRDBTestCase { waitForExpectations(timeout: 1, handler: nil) XCTAssertEqual(changesCount, 2) } - - func testExtentNextTransaction() throws { - // We need something to change - let dbQueue = try makeDatabaseQueue() - try dbQueue.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } - - // Track reducer process - let notificationExpectation = expectation(description: "notification") - notificationExpectation.assertForOverFulfill = true - notificationExpectation.expectedFulfillmentCount = 2 - - // A reducer - let reducer = AnyValueReducer( - fetch: { _ in }, - value: { $0 }) - - // Create an observation - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .nextTransaction - - // Start observation - let observer = try observation.start(in: dbQueue) { - notificationExpectation.fulfill() - } - - try withExtendedLifetime(observer) { - // notified - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - } - - // not notified - try dbQueue.write { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - } - - waitForExpectations(timeout: 1, handler: nil) - } - } } diff --git a/Tests/GRDBTests/ValueObservationFetchTests.swift b/Tests/GRDBTests/ValueObservationFetchTests.swift index f306be6f37..11e86b0bb1 100644 --- a/Tests/GRDBTests/ValueObservationFetchTests.swift +++ b/Tests/GRDBTests/ValueObservationFetchTests.swift @@ -31,23 +31,23 @@ class ValueObservationFetchTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.writeWithoutTransaction { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "UPDATE t SET id = id") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "UPDATE t SET id = id") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1, 1, 2]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1, 1, 2]) } try test(makeDatabaseQueue()) @@ -63,23 +63,23 @@ class ValueObservationFetchTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 3 - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }).distinctUntilChanged() - observation.extent = .databaseLifetime - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.writeWithoutTransaction { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "UPDATE t SET id = id") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "UPDATE t SET id = id") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1, 2]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1, 2]) } try test(makeDatabaseQueue()) diff --git a/Tests/GRDBTests/ValueObservationMapTests.swift b/Tests/GRDBTests/ValueObservationMapTests.swift index 3158b624fd..d308871a38 100644 --- a/Tests/GRDBTests/ValueObservationMapTests.swift +++ b/Tests/GRDBTests/ValueObservationMapTests.swift @@ -33,24 +33,24 @@ class ValueObservationMapTests: GRDBTestCase { }) // Create an observation - var observation = ValueObservation + let observation = ValueObservation .tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) .map { count -> String in return "\(count)" } - observation.extent = .databaseLifetime // Start observation - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.writeWithoutTransaction { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, ["1", "2", "3"]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, ["1", "2", "3"]) } try test(makeDatabaseQueue()) @@ -59,12 +59,10 @@ class ValueObservationMapTests: GRDBTestCase { func testMapPreservesConfiguration() { var observation = ValueObservation.tracking(DatabaseRegion(), fetch: { _ in }) - observation.extent = .nextTransaction observation.requiresWriteAccess = true observation.scheduling = .unsafe(startImmediately: true) let mappedObservation = observation.map { _ in } - XCTAssertEqual(mappedObservation.extent, observation.extent) XCTAssertEqual(mappedObservation.requiresWriteAccess, observation.requiresWriteAccess) switch mappedObservation.scheduling { case .unsafe: diff --git a/Tests/GRDBTests/ValueObservationReadonlyTests.swift b/Tests/GRDBTests/ValueObservationReadonlyTests.swift index f5941cd8d7..b843b8bdf2 100644 --- a/Tests/GRDBTests/ValueObservationReadonlyTests.swift +++ b/Tests/GRDBTests/ValueObservationReadonlyTests.swift @@ -23,21 +23,21 @@ class ValueObservationReadonlyTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 2 - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { count in + let observer = try observation.start(in: dbQueue) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbQueue.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbQueue.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1]) } func testWriteObservationFailsByDefault() throws { @@ -79,19 +79,19 @@ class ValueObservationReadonlyTests: GRDBTestCase { try db.execute(sql: "DROP TABLE temp") return result }) - observation.extent = .databaseLifetime observation.requiresWriteAccess = true - _ = try observation.start(in: dbQueue) { count in + let observer = try observation.start(in: dbQueue) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbQueue.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbQueue.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1]) } func testWriteObservationIsWrappedInSavepoint() throws { diff --git a/Tests/GRDBTests/ValueObservationRecordTests.swift b/Tests/GRDBTests/ValueObservationRecordTests.swift index a9fe22c08d..75acc510d2 100644 --- a/Tests/GRDBTests/ValueObservationRecordTests.swift +++ b/Tests/GRDBTests/ValueObservationRecordTests.swift @@ -36,31 +36,31 @@ class ValueObservationRecordTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var observation = ValueObservation.trackingAll(SQLRequest(sql: "SELECT * FROM t ORDER BY id")) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { players in + let observation = ValueObservation.trackingAll(SQLRequest(sql: "SELECT * FROM t ORDER BY id")) + let observer = try observation.start(in: dbQueue) { players in results.append(players) notificationExpectation.fulfill() } - - try dbQueue.inDatabase { db in - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 - try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = - try db.inTransaction { // +1 - try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") - try db.execute(sql: "DELETE FROM t WHERE id = 3") - return .commit + try withExtendedLifetime(observer) { + try dbQueue.inDatabase { db in + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 + try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = + try db.inTransaction { // +1 + try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") + try db.execute(sql: "DELETE FROM t WHERE id = 3") + return .commit + } + try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 } - try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(results.map { $0.map { $0.row }}, [ + [], + [["id":1, "name":"foo"]], + [["id":1, "name":"foo"], ["id":2, "name":"bar"]], + [["id":2, "name":"bar"]]]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results.map { $0.map { $0.row }}, [ - [], - [["id":1, "name":"foo"]], - [["id":1, "name":"foo"], ["id":2, "name":"bar"]], - [["id":2, "name":"bar"]]]) } func testOne() throws { @@ -72,30 +72,30 @@ class ValueObservationRecordTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var observation = ValueObservation.trackingOne(SQLRequest(sql: "SELECT * FROM t ORDER BY id DESC")) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { player in + let observation = ValueObservation.trackingOne(SQLRequest(sql: "SELECT * FROM t ORDER BY id DESC")) + let observer = try observation.start(in: dbQueue) { player in results.append(player) notificationExpectation.fulfill() } - - try dbQueue.inDatabase { db in - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 - try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = - try db.inTransaction { // +1 - try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") - try db.execute(sql: "DELETE FROM t WHERE id = 3") - return .commit + try withExtendedLifetime(observer) { + try dbQueue.inDatabase { db in + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 + try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = + try db.inTransaction { // +1 + try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") + try db.execute(sql: "DELETE FROM t WHERE id = 3") + return .commit + } + try db.execute(sql: "DELETE FROM t") // -1 } - try db.execute(sql: "DELETE FROM t") // -1 + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(results.map { $0.map { $0.row }}, [ + nil, + ["id":1, "name":"foo"], + ["id":2, "name":"bar"], + nil]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results.map { $0.map { $0.row }}, [ - nil, - ["id":1, "name":"foo"], - ["id":2, "name":"bar"], - nil]) } } diff --git a/Tests/GRDBTests/ValueObservationReducerTests.swift b/Tests/GRDBTests/ValueObservationReducerTests.swift index c64700ab5d..f25ca82436 100644 --- a/Tests/GRDBTests/ValueObservationReducerTests.swift +++ b/Tests/GRDBTests/ValueObservationReducerTests.swift @@ -58,11 +58,10 @@ class ValueObservationReducerTests: GRDBTestCase { // Create an observation let request = SQLRequest(sql: "SELECT * FROM t") - var observation = ValueObservation.tracking(request, reducer: { _ in reducer }) - observation.extent = .databaseLifetime + let observation = ValueObservation.tracking(request, reducer: { _ in reducer }) // Start observation - _ = try observation.start( + let observer = try observation.start( in: dbWriter, onError: { errors.append($0) @@ -72,53 +71,53 @@ class ValueObservationReducerTests: GRDBTestCase { changes.append($0) notificationExpectation.fulfill() }) - - - // Test that default config synchronously notifies initial value - XCTAssertEqual(fetchCount, 1) - XCTAssertEqual(reduceCount, 1) - XCTAssertEqual(errors.count, 0) - XCTAssertEqual(changes, ["0"]) - - try dbWriter.writeWithoutTransaction { db in - // Test a 1st notified transaction - try db.inTransaction { - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - return .commit - } - - // Test an untracked transaction - try db.inTransaction { - try db.execute(sql: "CREATE TABLE ignored(a)") - return .commit - } + try withExtendedLifetime(observer) { + // Test that default config synchronously notifies initial value + XCTAssertEqual(fetchCount, 1) + XCTAssertEqual(reduceCount, 1) + XCTAssertEqual(errors.count, 0) + XCTAssertEqual(changes, ["0"]) - // Test a dropped transaction - try db.inTransaction { - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - return .commit - } - - // Test a rollbacked transaction - try db.inTransaction { - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - return .rollback + try dbWriter.writeWithoutTransaction { db in + // Test a 1st notified transaction + try db.inTransaction { + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + return .commit + } + + // Test an untracked transaction + try db.inTransaction { + try db.execute(sql: "CREATE TABLE ignored(a)") + return .commit + } + + // Test a dropped transaction + try db.inTransaction { + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + return .commit + } + + // Test a rollbacked transaction + try db.inTransaction { + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + return .rollback + } + + // Test a 2nd notified transaction + try db.inTransaction { + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + return .commit + } } - // Test a 2nd notified transaction - try db.inTransaction { - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - return .commit - } + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(fetchCount, 4) + XCTAssertEqual(reduceCount, 4) + XCTAssertEqual(errors.count, 0) + XCTAssertEqual(changes, ["0", "1", "5"]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(fetchCount, 4) - XCTAssertEqual(reduceCount, 4) - XCTAssertEqual(errors.count, 0) - XCTAssertEqual(changes, ["0", "1", "5"]) } try test(makeDatabaseQueue()) @@ -174,11 +173,10 @@ class ValueObservationReducerTests: GRDBTestCase { value: { $0 }) // Create an observation - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .databaseLifetime + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) // Start observation - _ = try observation.start( + let observer = try observation.start( in: dbWriter, onError: { errors.append($0) @@ -189,16 +187,18 @@ class ValueObservationReducerTests: GRDBTestCase { notificationExpectation.fulfill() }) - struct TestError: Error { } - nextError = TestError() - try dbWriter.writeWithoutTransaction { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + struct TestError: Error { } + nextError = TestError() + try dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(changes.count, 2) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(changes.count, 2) } try test(makeDatabaseQueue()) @@ -225,21 +225,21 @@ class ValueObservationReducerTests: GRDBTestCase { value: { _ -> Int? in defer { count += 1 } return count }) - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer}) - observation.extent = .databaseLifetime + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer}) // Start observation - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.write { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.write { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1]) } try test(makeDatabaseQueue()) @@ -272,22 +272,22 @@ class ValueObservationReducerTests: GRDBTestCase { } // Create an observation - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .databaseLifetime + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) // Start observation - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.writeWithoutTransaction { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, ["1", "2", "3"]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, ["1", "2", "3"]) } try test(makeDatabaseQueue()) @@ -321,22 +321,22 @@ class ValueObservationReducerTests: GRDBTestCase { } // Create an observation - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .databaseLifetime + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) // Start observation - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.writeWithoutTransaction { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, ["1", "3"]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, ["1", "3"]) } try test(makeDatabaseQueue()) @@ -353,22 +353,22 @@ class ValueObservationReducerTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 3 struct T: TableRecord { } - var observation = ValueObservation + let observation = ValueObservation .trackingCount(T.all()) .map { "\($0)" } - observation.extent = .databaseLifetime - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.writeWithoutTransaction { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, ["0", "1", "2"]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, ["0", "1", "2"]) } try test(makeDatabaseQueue()) @@ -392,16 +392,16 @@ class ValueObservationReducerTests: GRDBTestCase { } reduceExpectation.fulfill() }) - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbWriter, onChange: { _ in }) - - try dbWriter.write { db in - try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) + let observer = try observation.start(in: dbWriter, onChange: { _ in }) + try withExtendedLifetime(observer) { + try dbWriter.write { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(labels, expectedLabels) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(labels, expectedLabels) } do { // dbQueue with default label diff --git a/Tests/GRDBTests/ValueObservationRowTests.swift b/Tests/GRDBTests/ValueObservationRowTests.swift index 83daddd157..f95c0cca19 100644 --- a/Tests/GRDBTests/ValueObservationRowTests.swift +++ b/Tests/GRDBTests/ValueObservationRowTests.swift @@ -22,31 +22,31 @@ class ValueObservationRowTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var observation = ValueObservation.trackingAll(SQLRequest(sql: "SELECT * FROM t ORDER BY id")) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { rows in + let observation = ValueObservation.trackingAll(SQLRequest(sql: "SELECT * FROM t ORDER BY id")) + let observer = try observation.start(in: dbQueue) { rows in results.append(rows) notificationExpectation.fulfill() } - - try dbQueue.inDatabase { db in - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 - try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = - try db.inTransaction { // +1 - try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") - try db.execute(sql: "DELETE FROM t WHERE id = 3") - return .commit + try withExtendedLifetime(observer) { + try dbQueue.inDatabase { db in + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 + try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = + try db.inTransaction { // +1 + try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") + try db.execute(sql: "DELETE FROM t WHERE id = 3") + return .commit + } + try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 } - try db.execute(sql: "DELETE FROM t WHERE id = 1") // -1 + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(results, [ + [], + [["id":1, "name":"foo"]], + [["id":1, "name":"foo"], ["id":2, "name":"bar"]], + [["id":2, "name":"bar"]]]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results, [ - [], - [["id":1, "name":"foo"]], - [["id":1, "name":"foo"], ["id":2, "name":"bar"]], - [["id":2, "name":"bar"]]]) } func testOne() throws { @@ -58,23 +58,23 @@ class ValueObservationRowTests: GRDBTestCase { notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var observation = ValueObservation.trackingOne(SQLRequest(sql: "SELECT * FROM t ORDER BY id DESC")) - observation.extent = .databaseLifetime - _ = try observation.start(in: dbQueue) { row in + let observation = ValueObservation.trackingOne(SQLRequest(sql: "SELECT * FROM t ORDER BY id DESC")) + let observer = try observation.start(in: dbQueue) { row in results.append(row) notificationExpectation.fulfill() } - - try dbQueue.inDatabase { db in - try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 - try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = - try db.inTransaction { // +1 - try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") - try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") - try db.execute(sql: "DELETE FROM t WHERE id = 3") - return .commit + try withExtendedLifetime(observer) { + try dbQueue.inDatabase { db in + try db.execute(sql: "INSERT INTO t (id, name) VALUES (1, 'foo')") // +1 + try db.execute(sql: "UPDATE t SET name = 'foo' WHERE id = 1") // = + try db.inTransaction { // +1 + try db.execute(sql: "INSERT INTO t (id, name) VALUES (2, 'bar')") + try db.execute(sql: "INSERT INTO t (id, name) VALUES (3, 'baz')") + try db.execute(sql: "DELETE FROM t WHERE id = 3") + return .commit + } + try db.execute(sql: "DELETE FROM t") // -1 } - try db.execute(sql: "DELETE FROM t") // -1 } waitForExpectations(timeout: 1, handler: nil) diff --git a/Tests/GRDBTests/ValueObservationSchedulingTests.swift b/Tests/GRDBTests/ValueObservationSchedulingTests.swift index 0901316cca..523a6c61ff 100644 --- a/Tests/GRDBTests/ValueObservationSchedulingTests.swift +++ b/Tests/GRDBTests/ValueObservationSchedulingTests.swift @@ -26,12 +26,10 @@ class ValueObservationSchedulingTests: GRDBTestCase { let key = DispatchSpecificKey<()>() DispatchQueue.main.setSpecific(key: key, value: ()) - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) - observation.extent = .databaseLifetime - - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) counts.append(count) notificationExpectation.fulfill() @@ -40,12 +38,14 @@ class ValueObservationSchedulingTests: GRDBTestCase { // dispatched when observation is started from the main queue XCTAssertEqual(counts, [0]) - try dbWriter.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1]) } try test(makeDatabaseQueue()) @@ -66,25 +66,25 @@ class ValueObservationSchedulingTests: GRDBTestCase { let key = DispatchSpecificKey<()>() DispatchQueue.main.setSpecific(key: key, value: ()) - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) - observation.extent = .databaseLifetime - + var observer: TransactionObserver? DispatchQueue.global(qos: .default).async { - _ = try! observation.start(in: dbWriter) { count in + observer = try! observation.start(in: dbWriter) { count in XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) counts.append(count) notificationExpectation.fulfill() } - try! dbWriter.write { try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") } } - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1]) + withExtendedLifetime(observer) { + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1]) + } } try test(makeDatabaseQueue()) @@ -113,10 +113,9 @@ class ValueObservationSchedulingTests: GRDBTestCase { } }, value: { $0 }) - var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .databaseLifetime + let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - _ = try observation.start( + let observer = try observation.start( in: dbWriter, onError: { error in XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) @@ -127,15 +126,16 @@ class ValueObservationSchedulingTests: GRDBTestCase { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) notificationExpectation.fulfill() }) - - struct TestError: Error { } - nextError = TestError() - try dbWriter.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + struct TestError: Error { } + nextError = TestError() + try dbWriter.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(errorCount, 1) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(errorCount, 1) } try test(makeDatabaseQueue()) @@ -159,21 +159,21 @@ class ValueObservationSchedulingTests: GRDBTestCase { var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) - observation.extent = .databaseLifetime observation.scheduling = .async(onQueue: queue, startImmediately: true) - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1]) } try test(makeDatabaseQueue()) @@ -197,21 +197,21 @@ class ValueObservationSchedulingTests: GRDBTestCase { var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) - observation.extent = .databaseLifetime observation.scheduling = .async(onQueue: queue, startImmediately: false) - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [1]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [1]) } try test(makeDatabaseQueue()) @@ -237,10 +237,9 @@ class ValueObservationSchedulingTests: GRDBTestCase { fetch: { _ in throw TestError() }, value: { $0 }) var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .databaseLifetime observation.scheduling = .async(onQueue: queue, startImmediately: false) - _ = try observation.start( + let observer = try observation.start( in: dbWriter, onError: { error in XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) @@ -248,13 +247,14 @@ class ValueObservationSchedulingTests: GRDBTestCase { notificationExpectation.fulfill() }, onChange: { _ in fatalError() }) - - try dbWriter.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(errorCount, 1) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(errorCount, 1) } try test(makeDatabaseQueue()) @@ -277,10 +277,9 @@ class ValueObservationSchedulingTests: GRDBTestCase { var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) - observation.extent = .databaseLifetime observation.scheduling = .unsafe(startImmediately: true) - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in if counts.isEmpty { // require main queue on first element XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) @@ -288,13 +287,14 @@ class ValueObservationSchedulingTests: GRDBTestCase { counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1]) } try test(makeDatabaseQueue()) @@ -318,11 +318,10 @@ class ValueObservationSchedulingTests: GRDBTestCase { var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) - observation.extent = .databaseLifetime observation.scheduling = .unsafe(startImmediately: true) - + var observer: TransactionObserver? queue.async { - _ = try! observation.start(in: dbWriter) { count in + observer = try! observation.start(in: dbWriter) { count in if counts.isEmpty { // require custom queue on first notification XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) @@ -330,14 +329,15 @@ class ValueObservationSchedulingTests: GRDBTestCase { counts.append(count) notificationExpectation.fulfill() } - try! dbWriter.write { try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") } } - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [0, 1]) + withExtendedLifetime(observer) { + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [0, 1]) + } } try test(makeDatabaseQueue()) @@ -357,20 +357,20 @@ class ValueObservationSchedulingTests: GRDBTestCase { var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }) - observation.extent = .databaseLifetime observation.scheduling = .unsafe(startImmediately: false) - _ = try observation.start(in: dbWriter) { count in + let observer = try observation.start(in: dbWriter) { count in counts.append(count) notificationExpectation.fulfill() } - - try dbWriter.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(counts, [1]) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(counts, [1]) } try test(makeDatabaseQueue()) @@ -392,23 +392,23 @@ class ValueObservationSchedulingTests: GRDBTestCase { fetch: { _ in throw TestError() }, value: { $0 }) var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, reducer: { _ in reducer }) - observation.extent = .databaseLifetime observation.scheduling = .unsafe(startImmediately: false) - _ = try observation.start( + let observer = try observation.start( in: dbWriter, onError: { error in errorCount += 1 notificationExpectation.fulfill() }, onChange: { _ in fatalError() }) - - try dbWriter.write { - try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + try withExtendedLifetime(observer) { + try dbWriter.write { + try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") + } + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(errorCount, 1) } - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(errorCount, 1) } try test(makeDatabaseQueue()) From 997be1871899b20a51d566ff6e306017be91b9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 1 Mar 2019 13:28:47 +0100 Subject: [PATCH 3/4] Update ValueObservation documentation --- README.md | 63 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index a94452dfc3..a9d06f2582 100644 --- a/README.md +++ b/README.md @@ -6073,7 +6073,36 @@ An initial fetch is performed as soon as the observation starts: the view is set The observer returned by the `start` method is stored in a property of the view controller. This allows the view controller to control the duration of the observation. When the observer is deallocated, the observation stops. Meanwhile, all transactions that modify the observed player are notified, and the `nameLabel` is kept up-to-date. > :bulb: **Tip**: see the [Demo Application](DemoApps/GRDBDemoiOS/README.md) for a sample app that uses ValueObservation. - +> +> :bulb: **Tip**: When fetching initial values is slow, and should not block the main queue, opt in for async notifications: +> +> ```swift +> override func viewWillAppear(_ animated: Bool) { +> super.viewWillAppear(animated) +> +> // Define a ValueObservation which tracks a player +> let request = Player.filter(key: 42) +> var observation = ValueObservation.trackingOne(request) +> +> // Observation is asynchronous +> observation.scheduling = .async(onQueue: .main, startImmediately: true) +> +> // Start observing the database +> observer = try! observation.start( +> in: dbQueue, +> onChange: { [unowned self] (player: Player?) in +> // Player has changed: update view +> self.activityIndicator.stopAnimating() +> self.nameLabel.text = player?.name +> }) +> +> // Wait for player +> activityIndicator.startAnimating() +> nameLabel.text = nil +> } +> ``` +> +> See [ValueObservation.scheduling](#valueobservationscheduling) for more information. ### ValueObservation.trackingCount, trackingOne, trackingAll @@ -6399,29 +6428,10 @@ let observer = try observation.start( Some behaviors of value observations can be configured: -- [ValueObservation.extent](#valueobservationextent): Precise control of the observation duration. - [ValueObservation.scheduling](#valueobservationscheduling): Control the dispatching of notified values. - [ValueObservation.requiresWriteAccess](#valueobservationrequireswriteaccess): Allow observations to write in the database. -#### ValueObservation.extent - -The `extent` property lets you specify the duration of the observation. - -The default extent is `.observerLifetime`: the observation stops when the observer returned by `start` is deallocated. - -You can use the `.databaseLifetime` extent to specify that the observation lasts until the database connection is closed: - -```swift -// No need to retain the observer returned by the start method: -var observation = ValueObservation... -observation.extent = .databaseLifetime -_ = observation.start(in: dbQueue) { newValue in ... } -``` - -> :warning: **Warning**: Don't use the `.nextTransaction` lifetime, because it produces unreliable results. A future version of GRDB will deprecate `ValueObservation.extent`, and provide a better API. - - #### ValueObservation.scheduling The `scheduling` property lets you control how fresh values are notified: @@ -6462,18 +6472,21 @@ The `scheduling` property lets you control how fresh values are notified: } ``` -- `.onQueue(_:startImmediately:)`: all values are asychronously notified on the specified queue. +- `.async(onQueue:startImmediately:)`: all values are asychronously notified on the specified queue. An initial value is fetched and notified if `startImmediately` is true. + For example: + ```swift - let customQueue = DispatchQueue(label: "customQueue") + // On main queue var observation = ValueObservation.trackingAll(Player.all()) - observation.scheduling = .onQueue(customQueue, startImmediately: true) + observation.scheduling = .async(onQueue: .main, startImmediately: true) let observer = try observation.start(in: dbQueue) { (players: [Player]) in - // On customQueue + // On main queue print("fresh players: \(players)")s } + // <- here "fresh players" is not printed yet. ``` - `unsafe(startImmediately:)`: values are not all notified on the same dispatch queue. @@ -7183,8 +7196,6 @@ dbQueue.inDatabase { db in } ``` -> :point_up: **Note**: removing a transaction observer makes it sure it won't be notified of any future transaction - there is no race condition. However, this does not stop eventual concurrent processing of previous transactions. For example, `remove(transactionObserver:)` is not a correct way to stop observers started by [ValueObservation]. In this case, the correct way is deallocating the observer. - Alternatively, use the `extent` parameter of the `add(transactionObserver:extent:)` method: ```swift From 4ee2523d596900b0d4b44ea5e867c252f71ae691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 1 Mar 2019 13:51:22 +0100 Subject: [PATCH 4/4] CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94056608f2..bd01833b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,13 +53,13 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one expection: - [#478](https://github.com/groue/GRDB.swift/pull/478): Swift 5: SQL interpolation - [#484](https://github.com/groue/GRDB.swift/pull/484): SE-0193 Cross-module inlining and specialization - [#486](https://github.com/groue/GRDB.swift/pull/486): Refactor PersistenceError.recordNotFound +- [#488](https://github.com/groue/GRDB.swift/pull/488): ValueObservation Cleanup ### Breaking Changes - Swift 4.0 and Swift 4.1 are no longer supported - iOS 8 is no longer supported. Minimum deployment target is now iOS 9.0 - Deprecated APIs are no longer available. -- `SQLRequest.arguments` is no longer optional ### Documentation Diff