From 9014b3ebd0a971a350087bb5214bd0134694767e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 26 Mar 2022 14:48:27 +0100 Subject: [PATCH 1/4] Fix #1192 --- .../SharedValueObservationTests.swift | 432 +++++++++--------- 1 file changed, 226 insertions(+), 206 deletions(-) diff --git a/Tests/GRDBTests/SharedValueObservationTests.swift b/Tests/GRDBTests/SharedValueObservationTests.swift index aad4ed0c46..c6a915d837 100644 --- a/Tests/GRDBTests/SharedValueObservationTests.swift +++ b/Tests/GRDBTests/SharedValueObservationTests.swift @@ -20,32 +20,36 @@ class SharedValueObservationTests: GRDBTestCase { extent: .observationLifetime) XCTAssertEqual(log.flush(), []) - do { - var value: Int? - let cancellable = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) - - XCTAssertEqual(value, 0) - XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) + // We want to control when the shared observation is deallocated + withExtendedLifetime(sharedObservation) { sharedObservation in + do { + var value: Int? + let cancellable = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value = $0 }) + + XCTAssertEqual(value, 0) + XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) + + cancellable.cancel() + XCTAssertEqual(log.flush(), []) + } - cancellable.cancel() - XCTAssertEqual(log.flush(), []) + do { + var value: Int? + let cancellable = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value = $0 }) + + XCTAssertEqual(value, 0) + XCTAssertEqual(log.flush(), []) + + cancellable.cancel() + XCTAssertEqual(log.flush(), []) + } } - do { - var value: Int? - let cancellable = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) - - XCTAssertEqual(value, 0) - XCTAssertEqual(log.flush(), []) - - cancellable.cancel() - XCTAssertEqual(log.flush(), []) - } - + // Deallocate the shared observation sharedObservation = nil XCTAssertEqual(log.flush(), ["cancel"]) } @@ -68,28 +72,32 @@ class SharedValueObservationTests: GRDBTestCase { extent: .observationLifetime) XCTAssertEqual(log.flush(), []) - do { - var value1: Int? - var value2: Int? - let cancellable1 = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value in - value1 = value - _ = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value in - value2 = value - }) - }) - - XCTAssertEqual(value1, 0) - XCTAssertEqual(value2, 0) - XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) - - cancellable1.cancel() - XCTAssertEqual(log.flush(), []) + // We want to control when the shared observation is deallocated + withExtendedLifetime(sharedObservation) { sharedObservation in + do { + var value1: Int? + var value2: Int? + let cancellable1 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + value1 = value + _ = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + value2 = value + }) + }) + + XCTAssertEqual(value1, 0) + XCTAssertEqual(value2, 0) + XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) + + cancellable1.cancel() + XCTAssertEqual(log.flush(), []) + } } + // Deallocate the shared observation sharedObservation = nil XCTAssertEqual(log.flush(), ["cancel"]) } @@ -120,7 +128,7 @@ class SharedValueObservationTests: GRDBTestCase { try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) } - + do { let recorder = publisher.record() try XCTAssertEqual(recorder.next().get(), 1) @@ -148,32 +156,36 @@ class SharedValueObservationTests: GRDBTestCase { extent: .whileObserved) XCTAssertEqual(log.flush(), []) - do { - var value: Int? - let cancellable = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) - - XCTAssertEqual(value, 0) - XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) + // We want to control when the shared observation is deallocated + withExtendedLifetime(sharedObservation) { sharedObservation in + do { + var value: Int? + let cancellable = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value = $0 }) + + XCTAssertEqual(value, 0) + XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) + + cancellable.cancel() + XCTAssertEqual(log.flush(), ["cancel"]) + } - cancellable.cancel() - XCTAssertEqual(log.flush(), ["cancel"]) + do { + var value: Int? + let cancellable = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value = $0 }) + + XCTAssertEqual(value, 0) + XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) + + cancellable.cancel() + XCTAssertEqual(log.flush(), ["cancel"]) + } } - do { - var value: Int? - let cancellable = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) - - XCTAssertEqual(value, 0) - XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) - - cancellable.cancel() - XCTAssertEqual(log.flush(), ["cancel"]) - } - + // Deallocate the shared observation sharedObservation = nil XCTAssertEqual(log.flush(), []) } @@ -196,7 +208,8 @@ class SharedValueObservationTests: GRDBTestCase { extent: .whileObserved) XCTAssertEqual(log.flush(), []) - do { + // We want to control when the shared observation is deallocated + withExtendedLifetime(sharedObservation) { sharedObservation in var value1: Int? var value2: Int? let cancellable1 = sharedObservation!.start( @@ -218,10 +231,11 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), ["cancel"]) } + // Deallocate the shared observation sharedObservation = nil XCTAssertEqual(log.flush(), []) } - + func test_async_observationLifetime() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -240,79 +254,82 @@ class SharedValueObservationTests: GRDBTestCase { extent: .observationLifetime) XCTAssertEqual(log.flush(), []) - // --- Start observation 1 - var values1: [Int] = [] - let exp1 = expectation(description: "") - exp1.expectedFulfillmentCount = 2 - exp1.assertForOverFulfill = false - let cancellable1 = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values1.append($0) - exp1.fulfill() - }) - - try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} - wait(for: [exp1], timeout: 1) - XCTAssertEqual(values1, [0, 1]) - XCTAssertEqual(log.flush(), [ - "start", "fetch", "tracked region: player(*)", "value: 0", - "database did change", "fetch", "value: 1"]) - - // --- Start observation 2 - var values2: [Int] = [] - let exp2 = expectation(description: "") - exp2.expectedFulfillmentCount = 2 - exp2.assertForOverFulfill = false - let cancellable2 = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values2.append($0) - exp2.fulfill() - }) - - try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} - wait(for: [exp2], timeout: 1) - XCTAssertEqual(values1, [0, 1, 2]) - XCTAssertEqual(values2, [1, 2]) - XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) - - // --- Stop observation 1 - cancellable1.cancel() - XCTAssertEqual(log.flush(), []) - - // --- Start observation 3 - var values3: [Int] = [] - let exp3 = expectation(description: "") - exp3.expectedFulfillmentCount = 2 - exp3.assertForOverFulfill = false - let cancellable3 = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values3.append($0) - exp3.fulfill() - }) - - try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} - wait(for: [exp3], timeout: 1) - XCTAssertEqual(values1, [0, 1, 2]) - XCTAssertEqual(values2, [1, 2, 3]) - XCTAssertEqual(values3, [2, 3]) - XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) - - // --- Stop observation 2 - cancellable2.cancel() - XCTAssertEqual(log.flush(), []) - - // --- Stop observation 3 - cancellable3.cancel() - XCTAssertEqual(log.flush(), []) + // We want to control when the shared observation is deallocated + try withExtendedLifetime(sharedObservation) { sharedObservation in + // --- Start observation 1 + var values1: [Int] = [] + let exp1 = expectation(description: "") + exp1.expectedFulfillmentCount = 2 + exp1.assertForOverFulfill = false + let cancellable1 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { + values1.append($0) + exp1.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp1], timeout: 1) + XCTAssertEqual(values1, [0, 1]) + XCTAssertEqual(log.flush(), [ + "start", "fetch", "tracked region: player(*)", "value: 0", + "database did change", "fetch", "value: 1"]) + + // --- Start observation 2 + var values2: [Int] = [] + let exp2 = expectation(description: "") + exp2.expectedFulfillmentCount = 2 + exp2.assertForOverFulfill = false + let cancellable2 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { + values2.append($0) + exp2.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp2], timeout: 1) + XCTAssertEqual(values1, [0, 1, 2]) + XCTAssertEqual(values2, [1, 2]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) + + // --- Stop observation 1 + cancellable1.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Start observation 3 + var values3: [Int] = [] + let exp3 = expectation(description: "") + exp3.expectedFulfillmentCount = 2 + exp3.assertForOverFulfill = false + let cancellable3 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { + values3.append($0) + exp3.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp3], timeout: 1) + XCTAssertEqual(values1, [0, 1, 2]) + XCTAssertEqual(values2, [1, 2, 3]) + XCTAssertEqual(values3, [2, 3]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) + + // --- Stop observation 2 + cancellable2.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Stop observation 3 + cancellable3.cancel() + XCTAssertEqual(log.flush(), []) + } // --- Release shared observation sharedObservation = nil XCTAssertEqual(log.flush(), ["cancel"]) } - + func test_async_observationLifetime_early_release() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -387,7 +404,7 @@ class SharedValueObservationTests: GRDBTestCase { try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) } - + do { let recorder = publisher.record() try XCTAssert(recorder.availableElements.get().isEmpty) @@ -416,73 +433,76 @@ class SharedValueObservationTests: GRDBTestCase { extent: .whileObserved) XCTAssertEqual(log.flush(), []) - // --- Start observation 1 - var values1: [Int] = [] - let exp1 = expectation(description: "") - exp1.expectedFulfillmentCount = 2 - exp1.assertForOverFulfill = false - let cancellable1 = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values1.append($0) - exp1.fulfill() - }) - - try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} - wait(for: [exp1], timeout: 1) - XCTAssertEqual(values1, [0, 1]) - XCTAssertEqual(log.flush(), [ - "start", "fetch", "tracked region: player(*)", "value: 0", - "database did change", "fetch", "value: 1"]) - - // --- Start observation 2 - var values2: [Int] = [] - let exp2 = expectation(description: "") - exp2.expectedFulfillmentCount = 2 - exp2.assertForOverFulfill = false - let cancellable2 = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values2.append($0) - exp2.fulfill() - }) - - try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} - wait(for: [exp2], timeout: 1) - XCTAssertEqual(values1, [0, 1, 2]) - XCTAssertEqual(values2, [1, 2]) - XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) - - // --- Stop observation 1 - cancellable1.cancel() - XCTAssertEqual(log.flush(), []) - - // --- Start observation 3 - var values3: [Int] = [] - let exp3 = expectation(description: "") - exp3.expectedFulfillmentCount = 2 - exp3.assertForOverFulfill = false - let cancellable3 = sharedObservation!.start( - onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values3.append($0) - exp3.fulfill() - }) - - try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} - wait(for: [exp3], timeout: 1) - XCTAssertEqual(values1, [0, 1, 2]) - XCTAssertEqual(values2, [1, 2, 3]) - XCTAssertEqual(values3, [2, 3]) - XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) - - // --- Stop observation 2 - cancellable2.cancel() - XCTAssertEqual(log.flush(), []) - - // --- Stop observation 3 - cancellable3.cancel() - XCTAssertEqual(log.flush(), ["cancel"]) + // We want to control when the shared observation is deallocated + try withExtendedLifetime(sharedObservation) { sharedObservation in + // --- Start observation 1 + var values1: [Int] = [] + let exp1 = expectation(description: "") + exp1.expectedFulfillmentCount = 2 + exp1.assertForOverFulfill = false + let cancellable1 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { + values1.append($0) + exp1.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp1], timeout: 1) + XCTAssertEqual(values1, [0, 1]) + XCTAssertEqual(log.flush(), [ + "start", "fetch", "tracked region: player(*)", "value: 0", + "database did change", "fetch", "value: 1"]) + + // --- Start observation 2 + var values2: [Int] = [] + let exp2 = expectation(description: "") + exp2.expectedFulfillmentCount = 2 + exp2.assertForOverFulfill = false + let cancellable2 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { + values2.append($0) + exp2.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp2], timeout: 1) + XCTAssertEqual(values1, [0, 1, 2]) + XCTAssertEqual(values2, [1, 2]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) + + // --- Stop observation 1 + cancellable1.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Start observation 3 + var values3: [Int] = [] + let exp3 = expectation(description: "") + exp3.expectedFulfillmentCount = 2 + exp3.assertForOverFulfill = false + let cancellable3 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { + values3.append($0) + exp3.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp3], timeout: 1) + XCTAssertEqual(values1, [0, 1, 2]) + XCTAssertEqual(values2, [1, 2, 3]) + XCTAssertEqual(values3, [2, 3]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) + + // --- Stop observation 2 + cancellable2.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Stop observation 3 + cancellable3.cancel() + XCTAssertEqual(log.flush(), ["cancel"]) + } // --- Release shared observation sharedObservation = nil @@ -600,7 +620,7 @@ class SharedValueObservationTests: GRDBTestCase { } } #endif - + #if compiler(>=5.5.2) && canImport(_Concurrency) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func testAsyncAwait() async throws { From 80c15c6ef60b7aa746562de84350daeb06efa4cb Mon Sep 17 00:00:00 2001 From: Renaud Lienhart Date: Thu, 31 Mar 2022 10:55:00 +1100 Subject: [PATCH 2/4] Fix a crash when an observation is quickly cancelled --- .../ValueWriteOnlyObserver.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/GRDB/ValueObservation/ValueWriteOnlyObserver.swift b/GRDB/ValueObservation/ValueWriteOnlyObserver.swift index 2856c0b019..46c884a6a2 100644 --- a/GRDB/ValueObservation/ValueWriteOnlyObserver.swift +++ b/GRDB/ValueObservation/ValueWriteOnlyObserver.swift @@ -198,7 +198,10 @@ extension ValueWriteOnlyObserver { // from a database access. try writer.unsafeReentrantWrite { db in // Fetch & Start observing the database - let fetchedValue = try fetchAndStartObservation(db) + guard let fetchedValue = try fetchAndStartObservation(db) else { + // Likely a GRDB bug + fatalError("can't start a cancelled or failed observation") + } // Reduce return reduceQueue.sync { @@ -218,12 +221,11 @@ extension ValueWriteOnlyObserver { // Start from a write access, so that self can register as a // transaction observer. writer.asyncWriteWithoutTransaction { db in - let isNotifying = self.lock.synchronized { self.notificationCallbacks != nil } - guard isNotifying else { return /* Cancelled */ } - do { // Fetch & Start observing the database - let fetchedValue = try self.fetchAndStartObservation(db) + guard let fetchedValue = try self.fetchAndStartObservation(db) else { + return /* Cancelled */ + } // Reduce // @@ -260,14 +262,13 @@ extension ValueWriteOnlyObserver { /// By grouping the initial fetch and the beginning of observation in a /// single database access, we are sure that no concurrent write can happen /// during the initial fetch, and that we won't miss any future change. - private func fetchAndStartObservation(_ db: Database) throws -> Reducer.Fetched { + private func fetchAndStartObservation(_ db: Database) throws -> Reducer.Fetched? { // TODO: [SR-214] remove -Opt suffix when we only support Xcode 12.5.1+ let (eventsOpt, fetchOpt) = lock.synchronized { (notificationCallbacks?.events, databaseAccess?.fetch) } guard let events = eventsOpt, let fetch = fetchOpt else { - // Likely a GRDB bug - fatalError("can't start a cancelled or failed observation") + return nil /* Cancelled */ } switch trackingMode { From feebc07101c06b49a2bb5ffd7e6b5778cb53b380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 2 Apr 2022 13:20:54 +0200 Subject: [PATCH 3/4] Documentation --- GRDB/ValueObservation/ValueConcurrentObserver.swift | 3 ++- GRDB/ValueObservation/ValueWriteOnlyObserver.swift | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/GRDB/ValueObservation/ValueConcurrentObserver.swift b/GRDB/ValueObservation/ValueConcurrentObserver.swift index cc829eb83a..232df1e4a3 100644 --- a/GRDB/ValueObservation/ValueConcurrentObserver.swift +++ b/GRDB/ValueObservation/ValueConcurrentObserver.swift @@ -192,7 +192,8 @@ extension ValueConcurrentObserver { (self.notificationCallbacks, self.databaseAccess) } guard let notificationCallbacks = notificationCallbacksOpt, let databaseAccess = databaseAccessOpt else { - // Likely a GRDB bug + // Likely a GRDB bug: during a synchronous start, user is not + // able to cancel observation. fatalError("can't start a cancelled or failed observation") } diff --git a/GRDB/ValueObservation/ValueWriteOnlyObserver.swift b/GRDB/ValueObservation/ValueWriteOnlyObserver.swift index 46c884a6a2..d41a2e9724 100644 --- a/GRDB/ValueObservation/ValueWriteOnlyObserver.swift +++ b/GRDB/ValueObservation/ValueWriteOnlyObserver.swift @@ -155,7 +155,8 @@ extension ValueWriteOnlyObserver { (self.notificationCallbacks, self.databaseAccess) } guard let notificationCallbacks = notificationCallbacksOpt, let writer = databaseAccessOpt?.writer else { - // Likely a GRDB bug + // Likely a GRDB bug: during a synchronous start, user is not + // able to cancel observation. fatalError("can't start a cancelled or failed observation") } @@ -199,7 +200,8 @@ extension ValueWriteOnlyObserver { try writer.unsafeReentrantWrite { db in // Fetch & Start observing the database guard let fetchedValue = try fetchAndStartObservation(db) else { - // Likely a GRDB bug + // Likely a GRDB bug: during a synchronous start, user is not + // able to cancel observation. fatalError("can't start a cancelled or failed observation") } @@ -259,6 +261,9 @@ extension ValueWriteOnlyObserver { /// Fetches the initial value, and start observing the database. /// + /// Returns nil if the observation was cancelled before database observation + /// could start. + /// /// By grouping the initial fetch and the beginning of observation in a /// single database access, we are sure that no concurrent write can happen /// during the initial fetch, and that we won't miss any future change. From 0324f592dc80a2eb43516ab1ca87380ae204f279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 2 Apr 2022 14:55:54 +0200 Subject: [PATCH 4/4] v5.22.2 --- CHANGELOG.md | 8 +++++++- GRDB.swift.podspec | 2 +- Makefile | 4 ++-- README.md | 4 ++-- Support/Info.plist | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5824b7f69e..63b4183a13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: #### 5.x Releases -- `5.22.x` Releases - [5.22.0](#5220) | [5.22.1](#5221) +- `5.22.x` Releases - [5.22.0](#5220) | [5.22.1](#5221) | [5.22.2](#5222) - `5.21.x` Releases - [5.21.0](#5210) - `5.20.x` Releases - [5.20.0](#5200) - `5.19.x` Releases - [5.19.0](#5190) @@ -89,6 +89,12 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: --- +## 5.22.2 + +Released April 2, 2022 • [diff](https://github.com/groue/GRDB.swift/compare/v5.22.1...v5.22.2) + +- **Fixed** a 5.22.0 regression: [#1196](https://github.com/groue/GRDB.swift/pull/1196) by [@layoutSubviews](https://github.com/layoutSubviews): Fix a crash when an observation is quickly cancelled + ## 5.22.1 Released March 26, 2022 • [diff](https://github.com/groue/GRDB.swift/compare/v5.22.0...v5.22.1) diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index 56c10c4ce8..4410aecc11 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GRDB.swift' - s.version = '5.22.1' + s.version = '5.22.2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'A toolkit for SQLite databases, with a focus on application development.' diff --git a/Makefile b/Makefile index c3a48ed1b7..e6fb8c3f64 100644 --- a/Makefile +++ b/Makefile @@ -538,8 +538,8 @@ ifdef JAZZY --author 'Gwendal Roué' \ --author_url https://github.com/groue \ --github_url https://github.com/groue/GRDB.swift \ - --github-file-prefix https://github.com/groue/GRDB.swift/tree/v5.22.1 \ - --module-version 5.22.1 \ + --github-file-prefix https://github.com/groue/GRDB.swift/tree/v5.22.2 \ + --module-version 5.22.2 \ --module GRDB \ --root-url http://groue.github.io/GRDB.swift/docs/5.22/ \ --output Documentation/Reference \ diff --git a/README.md b/README.md index 2a4d89484c..ab7f68e20e 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ --- -**Latest release**: March 26, 2022 • [version 5.22.1](https://github.com/groue/GRDB.swift/tree/v5.22.1) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 4 to GRDB 5](Documentation/GRDB5MigrationGuide.md) +**Latest release**: April 2, 2022 • [version 5.22.2](https://github.com/groue/GRDB.swift/tree/v5.22.2) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 4 to GRDB 5](Documentation/GRDB5MigrationGuide.md) **Requirements**: iOS 11.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ • SQLite 3.8.5+ • Swift 5.3+ / Xcode 12+ | Swift version | GRDB version | | -------------- | ----------------------------------------------------------- | -| **Swift 5.3+** | **v5.22.1** | +| **Swift 5.3+** | **v5.22.2** | | Swift 5.2 | [v5.12.0](https://github.com/groue/GRDB.swift/tree/v5.12.0) | | Swift 5.1 | [v4.14.0](https://github.com/groue/GRDB.swift/tree/v4.14.0) | | Swift 5 | [v4.14.0](https://github.com/groue/GRDB.swift/tree/v4.14.0) | diff --git a/Support/Info.plist b/Support/Info.plist index a2998f5487..a6a0d4de2e 100644 --- a/Support/Info.plist +++ b/Support/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.22.1 + 5.22.2 CFBundleSignature ???? CFBundleVersion